Merge pull request #1439 from turt2live/travis/pinned_messages

Message/event pinning
This commit is contained in:
Matthew Hodgson 2017-10-15 01:03:18 +01:00 committed by GitHub
commit 2b367edccf
9 changed files with 227 additions and 1 deletions

View File

@ -259,6 +259,11 @@ function textForPowerEvent(event) {
}); });
} }
function textForPinnedEvent(event) {
const senderName = event.getSender();
return _t("%(senderName)s changed the pinned messages for the room.", {senderName});
}
function textForWidgetEvent(event) { function textForWidgetEvent(event) {
const senderName = event.getSender(); const senderName = event.getSender();
const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent(); const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent();
@ -304,6 +309,7 @@ const stateHandlers = {
'm.room.history_visibility': textForHistoryVisibilityEvent, 'm.room.history_visibility': textForHistoryVisibilityEvent,
'm.room.encryption': textForEncryptionEvent, 'm.room.encryption': textForEncryptionEvent,
'm.room.power_levels': textForPowerEvent, 'm.room.power_levels': textForPowerEvent,
'm.room.pinned_events': textForPinnedEvent,
'im.vector.modular.widgets': textForWidgetEvent, 'im.vector.modular.widgets': textForWidgetEvent,
}; };

View File

@ -30,6 +30,10 @@ const FEATURES = [
id: 'feature_groups', id: 'feature_groups',
name: _td("Groups"), name: _td("Groups"),
}, },
{
id: 'feature_pinning',
name: _td("Message Pinning"),
},
]; ];
export default { export default {

View File

@ -1177,6 +1177,10 @@ module.exports = React.createClass({
return ret; return ret;
}, },
onPinnedClick: function() {
this.setState({showingPinned: !this.state.showingPinned, searching: false});
},
onSettingsClick: function() { onSettingsClick: function() {
this.showSettings(true); this.showSettings(true);
}, },
@ -1296,7 +1300,7 @@ module.exports = React.createClass({
}, },
onSearchClick: function() { onSearchClick: function() {
this.setState({ searching: true }); this.setState({ searching: true, showingPinned: false });
}, },
onCancelSearchClick: function() { onCancelSearchClick: function() {
@ -1495,6 +1499,7 @@ module.exports = React.createClass({
const RoomSettings = sdk.getComponent("rooms.RoomSettings"); const RoomSettings = sdk.getComponent("rooms.RoomSettings");
const AuxPanel = sdk.getComponent("rooms.AuxPanel"); const AuxPanel = sdk.getComponent("rooms.AuxPanel");
const SearchBar = sdk.getComponent("rooms.SearchBar"); const SearchBar = sdk.getComponent("rooms.SearchBar");
const PinnedEventsPanel = sdk.getComponent("rooms.PinnedEventsPanel");
const ScrollPanel = sdk.getComponent("structures.ScrollPanel"); const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
const TintableSvg = sdk.getComponent("elements.TintableSvg"); const TintableSvg = sdk.getComponent("elements.TintableSvg");
const RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar"); const RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar");
@ -1639,6 +1644,9 @@ module.exports = React.createClass({
} else if (this.state.searching) { } else if (this.state.searching) {
hideCancel = true; // has own cancel hideCancel = true; // has own cancel
aux = <SearchBar ref="search_bar" searchInProgress={this.state.searchInProgress} onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch} />; aux = <SearchBar ref="search_bar" searchInProgress={this.state.searchInProgress} onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch} />;
} else if (this.state.showingPinned) {
hideCancel = true; // has own cancel
aux = <PinnedEventsPanel room={this.state.room} onCancelClick={this.onPinnedClick} />;
} else if (!myMember || myMember.membership !== "join") { } else if (!myMember || myMember.membership !== "join") {
// We do have a room object for this room, but we're not currently in it. // We do have a room object for this room, but we're not currently in it.
// We may have a 3rd party invite to it. // We may have a 3rd party invite to it.
@ -1812,6 +1820,7 @@ module.exports = React.createClass({
collapsedRhs={this.props.collapsedRhs} collapsedRhs={this.props.collapsedRhs}
onSearchClick={this.onSearchClick} onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick} onSettingsClick={this.onSettingsClick}
onPinnedClick={this.onPinnedClick}
onSaveClick={this.onSettingsSaveClick} onSaveClick={this.onSettingsSaveClick}
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null} onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
onForgetClick={(myMember && myMember.membership === "leave") ? this.onForgetClick : null} onForgetClick={(myMember && myMember.membership === "leave") ? this.onForgetClick : null}

View File

@ -44,6 +44,7 @@ const eventTileTypes = {
'm.room.history_visibility': 'messages.TextualEvent', 'm.room.history_visibility': 'messages.TextualEvent',
'm.room.encryption': 'messages.TextualEvent', 'm.room.encryption': 'messages.TextualEvent',
'm.room.power_levels': 'messages.TextualEvent', 'm.room.power_levels': 'messages.TextualEvent',
'm.room.pinned_events' : 'messages.TextualEvent',
'im.vector.modular.widgets': 'messages.TextualEvent', 'im.vector.modular.widgets': 'messages.TextualEvent',
}; };

View File

@ -0,0 +1,89 @@
/*
Copyright 2017 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import MatrixClientPeg from "../../../MatrixClientPeg";
import dis from "../../../dispatcher";
import AccessibleButton from "../elements/AccessibleButton";
import MessageEvent from "../messages/MessageEvent";
import MemberAvatar from "../avatars/MemberAvatar";
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'PinnedEventTile',
propTypes: {
mxRoom: React.PropTypes.object.isRequired,
mxEvent: React.PropTypes.object.isRequired,
onUnpinned: React.PropTypes.func,
},
onTileClicked: function() {
dis.dispatch({
action: 'view_room',
event_id: this.props.mxEvent.getId(),
highlighted: true,
room_id: this.props.mxEvent.getRoomId(),
});
},
onUnpinClicked: function() {
const pinnedEvents = this.props.mxRoom.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
// Nothing to do: already unpinned
if (this.props.onUnpinned) this.props.onUnpinned();
} else {
const pinned = pinnedEvents.getContent().pinned;
const index = pinned.indexOf(this.props.mxEvent.getId());
if (index !== -1) {
pinned.splice(index, 1);
MatrixClientPeg.get().sendStateEvent(this.props.mxRoom.roomId, 'm.room.pinned_events', {pinned}, '').then(() => {
if (this.props.onUnpinned) this.props.onUnpinned();
});
} else if (this.props.onUnpinned) this.props.onUnpinned();
}
},
_canUnpin: function() {
return this.props.mxRoom.currentState.mayClientSendStateEvent('m.room.pinned_events', MatrixClientPeg.get());
},
render: function() {
const sender = this.props.mxRoom.getMember(this.props.mxEvent.getSender());
const avatarSize = 40;
let unpinButton = null;
if (this._canUnpin()) {
unpinButton = (
<AccessibleButton onClick={this.onUnpinClicked} className="mx_PinnedEventTile_unpinButton">
<img src="img/cancel-red.svg" width="8" height="8" alt={_t('Unpin Message')} title={_t('Unpin Message')} />
</AccessibleButton>
);
}
return (
<div className="mx_PinnedEventTile">
<div className="mx_PinnedEventTile_actions">
<AccessibleButton className="mx_PinnedEventTile_gotoButton mx_textButton" onClick={this.onTileClicked}>
{ _t("Jump to message") }
</AccessibleButton>
{ unpinButton }
</div>
<MemberAvatar member={sender} width={avatarSize} height={avatarSize} />
<span className="mx_PinnedEventTile_sender">
{sender.name}
</span>
<MessageEvent mxEvent={this.props.mxEvent} className="mx_PinnedEventTile_body" />
</div>
);
}
});

View File

@ -0,0 +1,101 @@
/*
Copyright 2017 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import MatrixClientPeg from "../../../MatrixClientPeg";
import AccessibleButton from "../elements/AccessibleButton";
import PinnedEventTile from "./PinnedEventTile";
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'PinnedEventsPanel',
propTypes: {
// The Room from the js-sdk we're going to show pinned events for
room: React.PropTypes.object.isRequired,
onCancelClick: React.PropTypes.func,
},
getInitialState: function() {
return {
loading: true,
};
},
componentDidMount: function() {
this._updatePinnedMessages();
},
_updatePinnedMessages: function() {
const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
this.setState({ loading: false, pinned: [] });
} else {
const promises = [];
const cli = MatrixClientPeg.get();
pinnedEvents.getContent().pinned.map(eventId => {
promises.push(cli.getEventTimeline(this.props.room.getUnfilteredTimelineSet(), eventId, 0).then(timeline => {
const event = timeline.getEvents().find(e => e.getId() === eventId);
return {eventId, timeline, event};
}).catch(err => {
console.error("Error looking up pinned event " + eventId + " in room " + this.props.room.roomId);
console.error(err);
return null; // return lack of context to avoid unhandled errors
}));
});
Promise.all(promises).then(contexts => {
// Filter out the messages before we try to render them
const pinned = contexts.filter(context => {
if (!context) return false; // no context == not applicable for the room
if (context.event.getType() !== "m.room.message") return false;
if (context.event.isRedacted()) return false;
return true;
});
this.setState({ loading: false, pinned });
});
}
},
_getPinnedTiles: function() {
if (this.state.pinned.length == 0) {
return (<div>{ _t("No pinned messages.") }</div>);
}
return this.state.pinned.map(context => {
return (<PinnedEventTile key={context.event.getId()} mxRoom={this.props.room} mxEvent={context.event} onUnpinned={this._updatePinnedMessages} />);
});
},
render: function() {
let tiles = <div>{ _t("Loading...") }</div>;
if (this.state && !this.state.loading) {
tiles = this._getPinnedTiles();
}
return (
<div className="mx_PinnedEventsPanel">
<div className="mx_PinnedEventsPanel_body">
<AccessibleButton className="mx_PinnedEventsPanel_cancel" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" /></AccessibleButton>
<h3 className="mx_PinnedEventsPanel_header">{_t("Pinned Messages")}</h3>
{ tiles }
</div>
</div>
);
}
});

View File

@ -31,6 +31,7 @@ import linkifyMatrix from '../../../linkify-matrix';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import ManageIntegsButton from '../elements/ManageIntegsButton'; import ManageIntegsButton from '../elements/ManageIntegsButton';
import {CancelButton} from './SimpleRoomHeader'; import {CancelButton} from './SimpleRoomHeader';
import UserSettingsStore from "../../../UserSettingsStore";
linkifyMatrix(linkify); linkifyMatrix(linkify);
@ -45,6 +46,7 @@ module.exports = React.createClass({
inRoom: React.PropTypes.bool, inRoom: React.PropTypes.bool,
collapsedRhs: React.PropTypes.bool, collapsedRhs: React.PropTypes.bool,
onSettingsClick: React.PropTypes.func, onSettingsClick: React.PropTypes.func,
onPinnedClick: React.PropTypes.func,
onSaveClick: React.PropTypes.func, onSaveClick: React.PropTypes.func,
onSearchClick: React.PropTypes.func, onSearchClick: React.PropTypes.func,
onLeaveClick: React.PropTypes.func, onLeaveClick: React.PropTypes.func,
@ -176,6 +178,7 @@ module.exports = React.createClass({
let spinner = null; let spinner = null;
let saveButton = null; let saveButton = null;
let settingsButton = null; let settingsButton = null;
let pinnedEventsButton = null;
let canSetRoomName; let canSetRoomName;
let canSetRoomAvatar; let canSetRoomAvatar;
@ -298,6 +301,13 @@ module.exports = React.createClass({
</AccessibleButton>; </AccessibleButton>;
} }
if (this.props.onPinnedClick && UserSettingsStore.isFeatureEnabled('feature_pinning')) {
pinnedEventsButton =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onPinnedClick} title={_t("Pinned Messages")}>
<TintableSvg src="img/icons-pin.svg" width="16" height="16" />
</AccessibleButton>;
}
// var leave_button; // var leave_button;
// if (this.props.onLeaveClick) { // if (this.props.onLeaveClick) {
// leave_button = // leave_button =
@ -342,6 +352,7 @@ module.exports = React.createClass({
rightRow = rightRow =
<div className="mx_RoomHeader_rightRow"> <div className="mx_RoomHeader_rightRow">
{ settingsButton } { settingsButton }
{ pinnedEventsButton }
{ manageIntegsButton } { manageIntegsButton }
{ forgetButton } { forgetButton }
{ searchButton } { searchButton }

View File

@ -619,6 +619,7 @@
"(~%(count)s results)|other": "(~%(count)s results)", "(~%(count)s results)|other": "(~%(count)s results)",
"Cancel": "Cancel", "Cancel": "Cancel",
"or": "or", "or": "or",
"Message Pinning": "Message Pinning",
"Active call": "Active call", "Active call": "Active call",
"Monday": "Monday", "Monday": "Monday",
"Tuesday": "Tuesday", "Tuesday": "Tuesday",
@ -889,6 +890,8 @@
"Add rooms to the group summary": "Add rooms to the group summary", "Add rooms to the group summary": "Add rooms to the group summary",
"Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?", "Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?",
"Room name or alias": "Room name or alias", "Room name or alias": "Room name or alias",
"Pinned Messages": "Pinned Messages",
"%(senderName)s changed the pinned messages for the room.": "%(senderName)s changed the pinned messages for the room.",
"You are an administrator of this group": "You are an administrator of this group", "You are an administrator of this group": "You are an administrator of this group",
"Failed to add the following rooms to the summary of %(groupId)s:": "Failed to add the following rooms to the summary of %(groupId)s:", "Failed to add the following rooms to the summary of %(groupId)s:": "Failed to add the following rooms to the summary of %(groupId)s:",
"Failed to remove the room from the summary of %(groupId)s": "Failed to remove the room from the summary of %(groupId)s", "Failed to remove the room from the summary of %(groupId)s": "Failed to remove the room from the summary of %(groupId)s",

View File

@ -844,6 +844,7 @@
"+example:%(domain)s": "+example:%(domain)s", "+example:%(domain)s": "+example:%(domain)s",
"Group IDs must be of the form +localpart:%(domain)s": "Group IDs must be of the form +localpart:%(domain)s", "Group IDs must be of the form +localpart:%(domain)s": "Group IDs must be of the form +localpart:%(domain)s",
"Room creation failed": "Room creation failed", "Room creation failed": "Room creation failed",
"Pinned Messages": "Pinned Messages",
"You are a member of these groups:": "You are a member of these groups:", "You are a member of these groups:": "You are a member of these groups:",
"Create a group to represent your community! Define a set of rooms and your own custom homepage to mark out your space in the Matrix universe.": "Create a group to represent your community! Define a set of rooms and your own custom homepage to mark out your space in the Matrix universe.", "Create a group to represent your community! Define a set of rooms and your own custom homepage to mark out your space in the Matrix universe.": "Create a group to represent your community! Define a set of rooms and your own custom homepage to mark out your space in the Matrix universe.",
"Join an existing group": "Join an existing group", "Join an existing group": "Join an existing group",
@ -859,6 +860,7 @@
"%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s", "%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s",
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>": "Robot check is currently unavailable on desktop - please use a <a>web browser</a>", "Robot check is currently unavailable on desktop - please use a <a>web browser</a>": "Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
"Verifies a user, device, and pubkey tuple": "Verifies a user, device, and pubkey tuple", "Verifies a user, device, and pubkey tuple": "Verifies a user, device, and pubkey tuple",
"%(senderName)s changed the pinned messages for the room.": "%(senderName)s changed the pinned messages for the room.",
"It is currently only possible to create groups on your own home server: use a group ID ending with %(domain)s": "It is currently only possible to create groups on your own home server: use a group ID ending with %(domain)s", "It is currently only possible to create groups on your own home server: use a group ID ending with %(domain)s": "It is currently only possible to create groups on your own home server: use a group ID ending with %(domain)s",
"To join an existing group you'll have to know its group identifier; this will look something like <i>+example:matrix.org</i>.": "To join an existing group you'll have to know its group identifier; this will look something like <i>+example:matrix.org</i>.", "To join an existing group you'll have to know its group identifier; this will look something like <i>+example:matrix.org</i>.": "To join an existing group you'll have to know its group identifier; this will look something like <i>+example:matrix.org</i>.",
"%(weekDayName)s, %(monthName)s %(day)s": "%(weekDayName)s, %(monthName)s %(day)s", "%(weekDayName)s, %(monthName)s %(day)s": "%(weekDayName)s, %(monthName)s %(day)s",