From 4f6cd6b23a8474550cdd745feafeb69dbc88bbf7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 15 Oct 2017 21:17:43 -0600 Subject: [PATCH 01/18] Add a small indicator for when a new event is pinned Signed-off-by: Travis Ralston --- .../views/rooms/PinnedEventsPanel.js | 19 ++++++++++ src/components/views/rooms/RoomHeader.js | 36 ++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/PinnedEventsPanel.js b/src/components/views/rooms/PinnedEventsPanel.js index deea03f030..5a99d9ab2d 100644 --- a/src/components/views/rooms/PinnedEventsPanel.js +++ b/src/components/views/rooms/PinnedEventsPanel.js @@ -71,6 +71,25 @@ module.exports = React.createClass({ this.setState({ loading: false, pinned }); }); } + + this._updateReadState(); + }, + + _updateReadState: function() { + const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", ""); + if (!pinnedEvents) return; // nothing to read + + let lastReadEvent = null; + const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins"); + if (readPinsEvent) { + lastReadEvent = readPinsEvent.getContent().last_read_id; + } + + if (lastReadEvent !== pinnedEvents.getId()) { + MatrixClientPeg.get().setRoomAccountData(this.props.room.roomId, "im.vector.room.read_pins", { + last_read_id: pinnedEvents.getId(), + }); + } }, _getPinnedTiles: function() { diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 4df0ff738c..ea5748db60 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -65,6 +65,7 @@ module.exports = React.createClass({ componentDidMount: function() { const cli = MatrixClientPeg.get(); cli.on("RoomState.events", this._onRoomStateEvents); + cli.on("Room.accountData", this._onRoomAccountData); // When a room name occurs, RoomState.events is fired *before* // room.name is updated. So we have to listen to Room.name as well as @@ -87,6 +88,7 @@ module.exports = React.createClass({ const cli = MatrixClientPeg.get(); if (cli) { cli.removeListener("RoomState.events", this._onRoomStateEvents); + cli.removeListener("Room.accountData", this._onRoomAccountData); } }, @@ -99,6 +101,13 @@ module.exports = React.createClass({ this._rateLimitedUpdate(); }, + _onRoomAccountData: function(event, room) { + if (!this.props.room || room.roomId !== this.props.room.roomId) return; + if (event.getType() !== "im.vector.room.read_pins") return; + + this._rateLimitedUpdate(); + }, + _rateLimitedUpdate: new RateLimitedFunc(function() { /* eslint-disable babel/no-invalid-this */ this.forceUpdate(); @@ -139,6 +148,25 @@ module.exports = React.createClass({ dis.dispatch({ action: 'show_right_panel' }); }, + _hasUnreadPins: function() { + const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", ''); + if (!currentPinEvent) return false; + if (currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0) { + return false; // no pins == nothing to read + } + + const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins"); + if (readPinsEvent) { + const lastReadEvent = readPinsEvent.getContent().last_read_id; + if (lastReadEvent) { + return currentPinEvent.getId() !== lastReadEvent; + } + } + + // There's pins, and we haven't read any of them + return true; + }, + /** * After editing the settings, get the new name for the room * @@ -302,8 +330,14 @@ module.exports = React.createClass({ } if (this.props.onPinnedClick && UserSettingsStore.isFeatureEnabled('feature_pinning')) { + let newPinsNotification = null; + if (this._hasUnreadPins()) { + newPinsNotification = (
); + } pinnedEventsButton = - + + { newPinsNotification } ; } From 3656fdb571a63b9686856386cb8d78c5c5a572c1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 3 Nov 2017 18:12:57 -0600 Subject: [PATCH 02/18] Store read pinned events as an array to avoid racing saves. Signed-off-by: Travis Ralston --- src/components/views/rooms/PinnedEventsPanel.js | 13 +++++++------ src/components/views/rooms/RoomHeader.js | 6 +++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/components/views/rooms/PinnedEventsPanel.js b/src/components/views/rooms/PinnedEventsPanel.js index 5a99d9ab2d..ddadc30258 100644 --- a/src/components/views/rooms/PinnedEventsPanel.js +++ b/src/components/views/rooms/PinnedEventsPanel.js @@ -79,21 +79,22 @@ module.exports = React.createClass({ const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", ""); if (!pinnedEvents) return; // nothing to read - let lastReadEvent = null; + let readStateEvents = null; const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins"); - if (readPinsEvent) { - lastReadEvent = readPinsEvent.getContent().last_read_id; + if (readPinsEvent && readPinsEvent.getContent()) { + readStateEvents = readPinsEvent.getContent().event_ids || []; } - if (lastReadEvent !== pinnedEvents.getId()) { + if (!readStateEvents.includes(pinnedEvents.getId())) { + readStateEvents.push(pinnedEvents.getId()); MatrixClientPeg.get().setRoomAccountData(this.props.room.roomId, "im.vector.room.read_pins", { - last_read_id: pinnedEvents.getId(), + event_ids: readStateEvents, }); } }, _getPinnedTiles: function() { - if (this.state.pinned.length == 0) { + if (this.state.pinned.length === 0) { return (
{ _t("No pinned messages.") }
); } diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index ea5748db60..2a06a90391 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -157,9 +157,9 @@ module.exports = React.createClass({ const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins"); if (readPinsEvent) { - const lastReadEvent = readPinsEvent.getContent().last_read_id; - if (lastReadEvent) { - return currentPinEvent.getId() !== lastReadEvent; + const readStateEvents = readPinsEvent.getContent().event_ids; + if (readStateEvents) { + return !readStateEvents.includes(currentPinEvent.getId()); } } From 5c37155730d343dc5e5f53ad2b12cb57d28aa9d7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 3 Nov 2017 18:18:09 -0600 Subject: [PATCH 03/18] Don't assume we have a valid event. Signed-off-by: Travis Ralston --- src/components/views/rooms/RoomHeader.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 2a06a90391..dcfb2e5a7c 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -156,8 +156,8 @@ module.exports = React.createClass({ } const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins"); - if (readPinsEvent) { - const readStateEvents = readPinsEvent.getContent().event_ids; + if (readPinsEvent && readPinsEvent.getContent()) { + const readStateEvents = readPinsEvent.getContent().event_ids || []; if (readStateEvents) { return !readStateEvents.includes(currentPinEvent.getId()); } From de6fc32a87d44bf63270366ecffe5e058949fc47 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 3 Nov 2017 20:00:48 -0600 Subject: [PATCH 04/18] Show an indicator when there are any pins in the room Signed-off-by: Travis Ralston --- .../views/rooms/PinnedEventsPanel.js | 8 ++--- src/components/views/rooms/RoomHeader.js | 16 ++++++++-- src/utils/PinningUtils.js | 30 +++++++++++++++++++ 3 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 src/utils/PinningUtils.js diff --git a/src/components/views/rooms/PinnedEventsPanel.js b/src/components/views/rooms/PinnedEventsPanel.js index d72e9a1b3f..5325768399 100644 --- a/src/components/views/rooms/PinnedEventsPanel.js +++ b/src/components/views/rooms/PinnedEventsPanel.js @@ -19,6 +19,7 @@ import MatrixClientPeg from "../../../MatrixClientPeg"; import AccessibleButton from "../elements/AccessibleButton"; import PinnedEventTile from "./PinnedEventTile"; import { _t } from '../../../languageHandler'; +import PinningUtils from "../../../utils/PinningUtils"; module.exports = React.createClass({ displayName: 'PinnedEventsPanel', @@ -61,12 +62,7 @@ module.exports = React.createClass({ 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; - }); + const pinned = contexts.filter((context) => PinningUtils.isPinnable(context.event)); this.setState({ loading: false, pinned }); }); diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 8374defe21..f558f44b4e 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -167,6 +167,13 @@ module.exports = React.createClass({ return true; }, + _hasPins: function() { + const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", ''); + if (!currentPinEvent) return false; + + return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0); + }, + /** * After editing the settings, get the new name for the room * @@ -333,14 +340,17 @@ module.exports = React.createClass({ } if (this.props.onPinnedClick && UserSettingsStore.isFeatureEnabled('feature_pinning')) { - let newPinsNotification = null; + let pinsIndicator = null; if (this._hasUnreadPins()) { - newPinsNotification = (
); + pinsIndicator = (
); + } else if (this._hasPins()) { + pinsIndicator = (
); } + pinnedEventsButton = - { newPinsNotification } + { pinsIndicator } ; } diff --git a/src/utils/PinningUtils.js b/src/utils/PinningUtils.js new file mode 100644 index 0000000000..90d26cc988 --- /dev/null +++ b/src/utils/PinningUtils.js @@ -0,0 +1,30 @@ +/* +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. +*/ + +export default class PinningUtils { + /** + * Determines if the given event may be pinned. + * @param {MatrixEvent} event The event to check. + * @return {boolean} True if the event may be pinned, false otherwise. + */ + static isPinnable(event) { + if (!event) return false; + if (event.getType() !== "m.room.message") return false; + if (event.isRedacted()) return false; + + return true; + } +} From 36cd22663a8c4eb2a526b28f64105c4c14647636 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 6 Nov 2017 18:02:50 +0000 Subject: [PATCH 05/18] Open group settings when the group is created --- src/components/structures/GroupView.js | 9 +++++++-- src/components/structures/LoggedInView.js | 1 + src/components/structures/MatrixChat.js | 5 ++++- src/components/views/dialogs/CreateGroupDialog.js | 1 + 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 24aa552890..88d10573cf 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -392,6 +392,8 @@ export default React.createClass({ propTypes: { groupId: PropTypes.string.isRequired, + // Whether this is the first time the group admin is viewing the group + groupIsNew: PropTypes.bool, }, childContextTypes: { @@ -422,7 +424,7 @@ export default React.createClass({ componentWillMount: function() { this._changeAvatarComponent = null; - this._initGroupStore(this.props.groupId); + this._initGroupStore(this.props.groupId, true); MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership); }, @@ -449,7 +451,7 @@ export default React.createClass({ this.setState({membershipBusy: false}); }, - _initGroupStore: function(groupId) { + _initGroupStore: function(groupId, firstInit) { this._groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId); this._groupStore.registerListener(() => { const summary = this._groupStore.getSummary(); @@ -472,6 +474,9 @@ export default React.createClass({ ), error: null, }); + if (this.props.groupIsNew && firstInit) { + this._onEditClick(); + } }); this._groupStore.on('error', (err) => { this.setState({ diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 5d1d47c5b2..08120d9508 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -301,6 +301,7 @@ export default React.createClass({ case PageTypes.GroupView: page_element = ; if (!this.props.collapseRhs) right_panel = ; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index e679276a08..e8ca8e82fc 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -490,7 +490,10 @@ module.exports = React.createClass({ case 'view_group': { const groupId = payload.group_id; - this.setState({currentGroupId: groupId}); + this.setState({ + currentGroupId: groupId, + currentGroupIsNew: payload.group_is_new, + }); this._setPage(PageTypes.GroupView); this.notifyNewScreen('group/' + groupId); } diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index e1dfe388d6..168fe75947 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -81,6 +81,7 @@ export default React.createClass({ dis.dispatch({ action: 'view_group', group_id: result.group_id, + group_is_new: true, }); this.props.onFinished(true); }).catch((e) => { From 137f1311b3bc2889732879b1ad40ef611e18cedd Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 6 Nov 2017 18:03:00 +0000 Subject: [PATCH 06/18] Add useful placeholder for the long description textarea --- src/components/structures/GroupView.js | 10 ++++++++++ src/i18n/strings/en_EN.json | 1 + 2 files changed, 11 insertions(+) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 88d10573cf..a76d825451 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -867,6 +867,16 @@ export default React.createClass({

{ _t("Long Description (HTML)") }