From 3a58e1c0665c3f0b12ec2d26723f0ceb61105268 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 28 Sep 2017 11:03:13 -0600 Subject: [PATCH 1/6] Show pinned message changes in the timeline Signed-off-by: Travis Ralston --- src/TextForEvent.js | 6 ++++++ src/components/views/rooms/EventTile.js | 1 + src/i18n/strings/en_EN.json | 1 + src/i18n/strings/en_US.json | 1 + 4 files changed, 9 insertions(+) diff --git a/src/TextForEvent.js b/src/TextForEvent.js index a21eb5c251..06b847c19a 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -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) { const senderName = event.getSender(); const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent(); @@ -301,6 +306,7 @@ const handlers = { 'm.room.history_visibility': textForHistoryVisibilityEvent, 'm.room.encryption': textForEncryptionEvent, 'm.room.power_levels': textForPowerEvent, + 'm.room.pinned_events': textForPinnedEvent, 'im.vector.modular.widgets': textForWidgetEvent, }; diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 7328cfe0b6..9bdee42cc1 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -44,6 +44,7 @@ var eventTileTypes = { 'm.room.history_visibility' : 'messages.TextualEvent', 'm.room.encryption' : 'messages.TextualEvent', 'm.room.power_levels' : 'messages.TextualEvent', + 'm.room.pinned_events' : 'messages.TextualEvent', 'im.vector.modular.widgets': 'messages.TextualEvent', }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 87fd6d4364..0cd70c91c3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -875,6 +875,7 @@ "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?", "Room name or alias": "Room name or alias", + "%(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", "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", diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index 928f1a9d0f..1ec7bbce83 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -852,6 +852,7 @@ "%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s", "Robot check is currently unavailable on desktop - please use a web browser": "Robot check is currently unavailable on desktop - please use a web browser", "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", "To join an existing group you'll have to know its group identifier; this will look something like +example:matrix.org.": "To join an existing group you'll have to know its group identifier; this will look something like +example:matrix.org." } From 874d383a8fdfaffbb6dd605e45209d663e2120db Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 28 Sep 2017 15:32:51 -0600 Subject: [PATCH 2/6] Add dock for pinned messages at the top of the room Signed-off-by: Travis Ralston --- src/components/structures/RoomView.js | 14 ++++++++++++-- src/components/views/rooms/RoomHeader.js | 10 ++++++++++ src/i18n/strings/en_EN.json | 1 + src/i18n/strings/en_US.json | 1 + 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index e0332b1b19..5b07459299 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1128,6 +1128,10 @@ module.exports = React.createClass({ return ret; }, + onPinnedClick: function() { + this.setState({showingPinned: !this.state.showingPinned, searching: false}); + }, + onSettingsClick: function() { this.showSettings(true); }, @@ -1248,7 +1252,7 @@ module.exports = React.createClass({ }, onSearchClick: function() { - this.setState({ searching: true }); + this.setState({ searching: true, showingPinned: false }); }, onCancelSearchClick: function() { @@ -1447,6 +1451,7 @@ module.exports = React.createClass({ const RoomSettings = sdk.getComponent("rooms.RoomSettings"); const AuxPanel = sdk.getComponent("rooms.AuxPanel"); const SearchBar = sdk.getComponent("rooms.SearchBar"); + const PinnedEventsPanel = sdk.getComponent("rooms.PinnedEventsPanel"); const ScrollPanel = sdk.getComponent("structures.ScrollPanel"); const TintableSvg = sdk.getComponent("elements.TintableSvg"); const RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar"); @@ -1587,7 +1592,11 @@ module.exports = React.createClass({ aux = ; } else if (this.state.searching) { hideCancel = true; // has own cancel - aux = ; + aux = ; + } else if (this.state.showingPinned) { + hideCancel = true; // has own cancel + aux = ; } else if (!myMember || myMember.membership !== "join") { // 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. @@ -1761,6 +1770,7 @@ module.exports = React.createClass({ collapsedRhs={ this.props.collapsedRhs } onSearchClick={this.onSearchClick} onSettingsClick={this.onSettingsClick} + onPinnedClick={this.onPinnedClick} onSaveClick={this.onSettingsSaveClick} onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null} onForgetClick={(myMember && myMember.membership === "leave") ? this.onForgetClick : null} diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 42cbb90cd9..3e02db5793 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -45,6 +45,7 @@ module.exports = React.createClass({ inRoom: React.PropTypes.bool, collapsedRhs: React.PropTypes.bool, onSettingsClick: React.PropTypes.func, + onPinnedClick: React.PropTypes.func, onSaveClick: React.PropTypes.func, onSearchClick: React.PropTypes.func, onLeaveClick: React.PropTypes.func, @@ -172,6 +173,7 @@ module.exports = React.createClass({ let spinner = null; let saveButton = null; let settingsButton = null; + let pinnedEventsButton = null; let canSetRoomName; let canSetRoomAvatar; @@ -290,6 +292,13 @@ module.exports = React.createClass({ ; } + if (this.props.onPinnedClick) { + pinnedEventsButton = + + + ; + } + // var leave_button; // if (this.props.onLeaveClick) { // leave_button = @@ -334,6 +343,7 @@ module.exports = React.createClass({ rightRow =
{ settingsButton } + { pinnedEventsButton } { manageIntegsButton } { forgetButton } { searchButton } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0cd70c91c3..7adcb02f64 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -875,6 +875,7 @@ "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?", "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", "Failed to add the following rooms to the summary of %(groupId)s:": "Failed to add the following rooms to the summary of %(groupId)s:", diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index 1ec7bbce83..98796ff32e 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -837,6 +837,7 @@ "+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", "Room creation failed": "Room creation failed", + "Pinned Messages": "Pinned Messages", "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.", "Join an existing group": "Join an existing group", From f71e07670d193e127836f769be9f70ddab5c9390 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 28 Sep 2017 17:00:26 -0600 Subject: [PATCH 3/6] Send toggle handler for the cancel button in the pinned events pane Signed-off-by: Travis Ralston --- src/components/structures/RoomView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 5b07459299..23e5833468 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1596,7 +1596,7 @@ module.exports = React.createClass({ onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch}/>; } else if (this.state.showingPinned) { hideCancel = true; // has own cancel - aux = ; + aux = ; } else if (!myMember || myMember.membership !== "join") { // 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. From 0f2fd9f69882b6fed86615d9b04e3b4c1a5d2cbd Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 29 Sep 2017 13:14:56 -0600 Subject: [PATCH 4/6] Move the PinnedEventsPanel to the react-sdk Signed-off-by: Travis Ralston --- src/components/views/rooms/PinnedEventTile.js | 86 +++++++++++++++ .../views/rooms/PinnedEventsPanel.js | 101 ++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 src/components/views/rooms/PinnedEventTile.js create mode 100644 src/components/views/rooms/PinnedEventsPanel.js diff --git a/src/components/views/rooms/PinnedEventTile.js b/src/components/views/rooms/PinnedEventTile.js new file mode 100644 index 0000000000..555718709b --- /dev/null +++ b/src/components/views/rooms/PinnedEventTile.js @@ -0,0 +1,86 @@ +/* +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 = {_t('Unpin; + } + + return ( +
+
+ + { _t("Jump to message") } + + { unpinButton } +
+ + + + {sender.name} + + +
+ ); + } +}); diff --git a/src/components/views/rooms/PinnedEventsPanel.js b/src/components/views/rooms/PinnedEventsPanel.js new file mode 100644 index 0000000000..02df29fc72 --- /dev/null +++ b/src/components/views/rooms/PinnedEventsPanel.js @@ -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 (
{ _t("No pinned messages.") }
); + } + + return this.state.pinned.map(context => { + return (); + }); + }, + + render: function() { + let tiles =
{ _t("Loading...") }
; + if (this.state && !this.state.loading) { + tiles = this._getPinnedTiles(); + } + + return ( +
+
+ +

{_t("Pinned Messages")}

+ { tiles } +
+
+ ); + } +}); From a01387f7a63757fc64eac730165a7d288c801ebd Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 29 Sep 2017 13:32:25 -0600 Subject: [PATCH 5/6] Use an AccessibleButton for unpinning from the pane. Signed-off-by: Travis Ralston --- src/components/views/rooms/PinnedEventTile.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/PinnedEventTile.js b/src/components/views/rooms/PinnedEventTile.js index 555718709b..e4b5b1ff96 100644 --- a/src/components/views/rooms/PinnedEventTile.js +++ b/src/components/views/rooms/PinnedEventTile.js @@ -62,8 +62,11 @@ module.exports = React.createClass({ let unpinButton = null; if (this._canUnpin()) { - unpinButton = {_t('Unpin; + unpinButton = ( + + {_t('Unpin + + ); } return ( From 8a641c7173dd39722322eac7abe4028ec639ee14 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 14 Oct 2017 16:40:10 -0600 Subject: [PATCH 6/6] Hide message pinning behind a labs setting Signed-off-by: Travis Ralston --- src/UserSettingsStore.js | 4 ++++ src/components/views/rooms/RoomHeader.js | 3 ++- src/i18n/strings/en_EN.json | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index f9d0a9cda8..b274e6a594 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -30,6 +30,10 @@ const FEATURES = [ id: 'feature_groups', name: _td("Groups"), }, + { + id: 'feature_pinning', + name: _td("Message Pinning"), + }, ]; export default { diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 59d8937aa6..4df0ff738c 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -31,6 +31,7 @@ import linkifyMatrix from '../../../linkify-matrix'; import AccessibleButton from '../elements/AccessibleButton'; import ManageIntegsButton from '../elements/ManageIntegsButton'; import {CancelButton} from './SimpleRoomHeader'; +import UserSettingsStore from "../../../UserSettingsStore"; linkifyMatrix(linkify); @@ -300,7 +301,7 @@ module.exports = React.createClass({ ; } - if (this.props.onPinnedClick) { + if (this.props.onPinnedClick && UserSettingsStore.isFeatureEnabled('feature_pinning')) { pinnedEventsButton = diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 95a8fcdd58..86ad0631e2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -618,6 +618,7 @@ "(~%(count)s results)|other": "(~%(count)s results)", "Cancel": "Cancel", "or": "or", + "Message Pinning": "Message Pinning", "Active call": "Active call", "Monday": "Monday", "Tuesday": "Tuesday",