From 4f6cd6b23a8474550cdd745feafeb69dbc88bbf7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 15 Oct 2017 21:17:43 -0600 Subject: [PATCH 01/29] 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 f032284eff219c279939740d65216cb49e50dfa8 Mon Sep 17 00:00:00 2001 From: "Andrew (anoa)" Date: Tue, 24 Oct 2017 16:21:46 -0700 Subject: [PATCH 02/29] Remember whether widget drawer was hidden per-room Fixes #4850 Signed-off-by: Andrew (anoa) --- src/components/structures/RoomView.js | 16 +++++++++++++++- src/components/views/rooms/AppsDrawer.js | 15 +++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 83ca987276..583ce78785 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -281,7 +281,7 @@ module.exports = React.createClass({ this.setState({ isPeeking: false, }); - + // This won't necessarily be a MatrixError, but we duck-type // here and say if it's got an 'errcode' key with the right value, // it means we can't peek. @@ -305,6 +305,20 @@ module.exports = React.createClass({ _shouldShowApps: function(room) { if (!BROWSER_SUPPORTS_SANDBOX) return false; + // Check if user has prompted to close this app before + // If so, do not show apps + let showWidget = localStorage.getItem( + room.roomId + "_show_widget_drawer"); + + console.warn(room); + console.warn("Key is: " + room.roomId + "_show_widget_drawer"); + console.warn("showWidget is: " + showWidget); + + if (showWidget == "false") { + console.warn("We're blocking the widget from loading."); + return false; + } + const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets'); // any valid widget = show apps for (let i = 0; i < appsStateEvents.length; i++) { diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 1c9296228d..9bc946bc4b 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -83,14 +83,25 @@ module.exports = React.createClass({ onAction: function(action) { switch (action.action) { case 'appsDrawer': - // When opening the app draw when there aren't any apps, auto-launch the - // integrations manager to skip the awkward click on "Add widget" + // When opening the app drawer when there aren't any apps, + // auto-launch the integrations manager to skip the awkward + // click on "Add widget" + let widgetStateKey = this.props.room.roomId + "_show_widget_drawer"; if (action.show) { const apps = this._getApps(); if (apps.length === 0) { this._launchManageIntegrations(); } + + localStorage.removeItem(widgetStateKey); + } else { + // Store hidden state of widget + // Don't show if previously hidden + console.warn("Storing hidden widget state for room - ", + this.props.room.roomId); + localStorage.setItem(widgetStateKey, false); } + break; } }, From 9821f0d459766dc2e1a3436c0978371981b3e42b Mon Sep 17 00:00:00 2001 From: "Andrew (anoa)" Date: Tue, 24 Oct 2017 16:37:23 -0700 Subject: [PATCH 03/29] Fix linting Signed-off-by: Andrew (anoa) --- src/components/views/rooms/AppsDrawer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 9bc946bc4b..09bf4e616b 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -81,12 +81,12 @@ module.exports = React.createClass({ }, onAction: function(action) { + const widgetStateKey = this.props.room.roomId + "_show_widget_drawer"; switch (action.action) { case 'appsDrawer': // When opening the app drawer when there aren't any apps, // auto-launch the integrations manager to skip the awkward // click on "Add widget" - let widgetStateKey = this.props.room.roomId + "_show_widget_drawer"; if (action.show) { const apps = this._getApps(); if (apps.length === 0) { From b4868a6846461918836f91b3b1364ad6c690785a Mon Sep 17 00:00:00 2001 From: "Andrew (anoa)" Date: Thu, 26 Oct 2017 11:17:13 -0700 Subject: [PATCH 04/29] showWidget->hideWidgetDrawer and remove logs Signed-off-by: Andrew (anoa) --- src/components/structures/RoomView.js | 15 +++++---------- src/components/views/rooms/AppsDrawer.js | 8 +++----- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 583ce78785..38603f1805 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -305,17 +305,12 @@ module.exports = React.createClass({ _shouldShowApps: function(room) { if (!BROWSER_SUPPORTS_SANDBOX) return false; - // Check if user has prompted to close this app before - // If so, do not show apps - let showWidget = localStorage.getItem( - room.roomId + "_show_widget_drawer"); + // Check if user has previously chosen to hide the app drawer for this + // room. If so, do not show apps + let hideWidgetDrawer = localStorage.getItem( + room.roomId + "_hide_widget_drawer"); - console.warn(room); - console.warn("Key is: " + room.roomId + "_show_widget_drawer"); - console.warn("showWidget is: " + showWidget); - - if (showWidget == "false") { - console.warn("We're blocking the widget from loading."); + if (hideWidgetDrawer === "true") { return false; } diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 09bf4e616b..9a3ba5f329 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -81,7 +81,7 @@ module.exports = React.createClass({ }, onAction: function(action) { - const widgetStateKey = this.props.room.roomId + "_show_widget_drawer"; + const hideWidgetKey = this.props.room.roomId + "_hide_widget_drawer"; switch (action.action) { case 'appsDrawer': // When opening the app drawer when there aren't any apps, @@ -93,13 +93,11 @@ module.exports = React.createClass({ this._launchManageIntegrations(); } - localStorage.removeItem(widgetStateKey); + localStorage.removeItem(hideWidgetKey); } else { // Store hidden state of widget // Don't show if previously hidden - console.warn("Storing hidden widget state for room - ", - this.props.room.roomId); - localStorage.setItem(widgetStateKey, false); + localStorage.setItem(hideWidgetKey, true); } break; From 112c74a255e9a74c8393dbc299b790e944f9482a Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 2 Nov 2017 18:54:25 +0000 Subject: [PATCH 05/29] Add NotifProvider to offer @room as a completion --- src/autocomplete/Autocompleter.js | 2 + src/autocomplete/NotifProvider.js | 63 +++++++++++++++++++++++++++++++ src/i18n/strings/en_EN.json | 2 + 3 files changed, 67 insertions(+) create mode 100644 src/autocomplete/NotifProvider.js diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index 94d2ed28de..3d30363d9f 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -23,6 +23,7 @@ import DuckDuckGoProvider from './DuckDuckGoProvider'; import RoomProvider from './RoomProvider'; import UserProvider from './UserProvider'; import EmojiProvider from './EmojiProvider'; +import NotifProvider from './NotifProvider'; import Promise from 'bluebird'; export type SelectionRange = { @@ -44,6 +45,7 @@ const PROVIDERS = [ UserProvider, RoomProvider, EmojiProvider, + NotifProvider, CommandProvider, DuckDuckGoProvider, ]; diff --git a/src/autocomplete/NotifProvider.js b/src/autocomplete/NotifProvider.js new file mode 100644 index 0000000000..fb33d0061b --- /dev/null +++ b/src/autocomplete/NotifProvider.js @@ -0,0 +1,63 @@ +/* +Copyright 2017 New Vector Ltd + +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 AutocompleteProvider from './AutocompleteProvider'; +import { _t } from '../languageHandler'; +import MatrixClientPeg from '../MatrixClientPeg'; +import {PillCompletion} from './Components'; +import sdk from '../index'; + +const AT_ROOM_REGEX = /@\S*/g; + +export default class NotifProvider extends AutocompleteProvider { + constructor(room) { + super(AT_ROOM_REGEX); + this.room = room; + } + + async getCompletions(query: string, selection: {start: number, end: number}, force = false) { + const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); + + const client = MatrixClientPeg.get(); + + if (!this.room.currentState.mayTriggerNotifOfType('room', client.credentials.userId)) return []; + + let completions = []; + const {command, range} = this.getCurrentCommand(query, selection, force); + if (command && command[0] && '@room'.startsWith(command[0]) && command[0].length > 1) { + return [{ + completion: '@room', + suffix: ' ', + component: ( + } title="@room" description={_t("Notify the whole room")} /> + ), + range, + }]; + } + return []; + } + + getName() { + return '❗️ ' + _t('Room Notification'); + } + + renderCompletions(completions: [React.Component]): ?React.Component { + return
+ { completions } +
; + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index bc2f0754a7..c1dd089d10 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -891,6 +891,8 @@ "Commands": "Commands", "Results from DuckDuckGo": "Results from DuckDuckGo", "Emoji": "Emoji", + "Notify the whole room": "Notify the whole room", + "Room Notification": "Room Notification", "Users": "Users", "unknown device": "unknown device", "NOT verified": "NOT verified", From b2cd65e182c7628469622d8f97646152406ffd81 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 3 Nov 2017 12:30:58 +0000 Subject: [PATCH 06/29] Fixes React warning Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/Dropdown.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js index 3787523a56..b1291710b7 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.js @@ -26,11 +26,9 @@ class MenuOption extends React.Component { this._onClick = this._onClick.bind(this); } - getDefaultProps() { - return { - disabled: false, - }; - } + static defaultProps = { + disabled: false, + }; _onMouseEnter() { this.props.onMouseEnter(this.props.dropdownKey); From 3656fdb571a63b9686856386cb8d78c5c5a572c1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 3 Nov 2017 18:12:57 -0600 Subject: [PATCH 07/29] 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 08/29] 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 09/29] 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 febeb0429eb9c582bf24cf60a14a26053c509b24 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 6 Nov 2017 10:18:10 +0000 Subject: [PATCH 10/29] Throw an error when trying to create a group store with falsey groupId --- src/stores/GroupStore.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js index 2578d373a7..11dd664053 100644 --- a/src/stores/GroupStore.js +++ b/src/stores/GroupStore.js @@ -33,6 +33,9 @@ export default class GroupStore extends EventEmitter { constructor(matrixClient, groupId) { super(); + if (!groupId) { + throw new Error('GroupStore needs a valid groupId to be created'); + } this.groupId = groupId; this._matrixClient = matrixClient; this._summary = {}; From adc42904516c1a5a5c84ee90666f8a48266750a0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 6 Nov 2017 15:11:42 +0000 Subject: [PATCH 11/29] Pillify room notif pills in composer --- .../views/rooms/MessageComposerInput.js | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 43f3aa5d88..42ab553b46 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -58,6 +58,9 @@ const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000; const ZWS_CODE = 8203; const ZWS = String.fromCharCode(ZWS_CODE); // zero width space + +const ATROOMPILL_ENTITY_TYPE = 'ATROOMPILL'; + function stateToMarkdown(state) { return __stateToMarkdown(state) .replace( @@ -188,13 +191,16 @@ export default class MessageComposerInput extends React.Component { this.client = MatrixClientPeg.get(); } - findLinkEntities(contentState: ContentState, contentBlock: ContentBlock, callback) { + findPillEntities(contentState: ContentState, contentBlock: ContentBlock, callback) { contentBlock.findEntityRanges( (character) => { const entityKey = character.getEntity(); return ( entityKey !== null && - contentState.getEntity(entityKey).getType() === 'LINK' + ( + contentState.getEntity(entityKey).getType() === 'LINK' || + contentState.getEntity(entityKey).getType() === ATROOMPILL_ENTITY_TYPE + ) ); }, callback, ); @@ -210,11 +216,19 @@ export default class MessageComposerInput extends React.Component { RichText.getScopedMDDecorators(this.props); const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false); decorators.push({ - strategy: this.findLinkEntities.bind(this), + strategy: this.findPillEntities.bind(this), component: (entityProps) => { const Pill = sdk.getComponent('elements.Pill'); + const type = entityProps.contentState.getEntity(entityProps.entityKey).getType(); const {url} = entityProps.contentState.getEntity(entityProps.entityKey).getData(); - if (Pill.isPillUrl(url)) { + if (type === ATROOMPILL_ENTITY_TYPE) { + return ; + } else if (Pill.isPillUrl(url)) { return { let blockText = block.getText(); let offset = 0; - this.findLinkEntities(contentState, block, (start, end) => { + this.findPillEntities(contentState, block, (start, end) => { const entity = contentState.getEntity(block.getEntityAt(start)); if (entity.getType() !== 'LINK') { return; @@ -989,6 +1003,11 @@ export default class MessageComposerInput extends React.Component { isCompletion: true, }); entityKey = contentState.getLastCreatedEntityKey(); + } else if (completion === '@room') { + contentState = contentState.createEntity(ATROOMPILL_ENTITY_TYPE, 'IMMUTABLE', { + isCompletion: true, + }); + entityKey = contentState.getLastCreatedEntityKey(); } let selection; From 41e7496ff1ead9faaa0fcee52c2e972d9f2984a0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 6 Nov 2017 15:25:25 +0000 Subject: [PATCH 12/29] unused var --- src/autocomplete/NotifProvider.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/autocomplete/NotifProvider.js b/src/autocomplete/NotifProvider.js index fb33d0061b..b7ac645525 100644 --- a/src/autocomplete/NotifProvider.js +++ b/src/autocomplete/NotifProvider.js @@ -36,7 +36,6 @@ export default class NotifProvider extends AutocompleteProvider { if (!this.room.currentState.mayTriggerNotifOfType('room', client.credentials.userId)) return []; - let completions = []; const {command, range} = this.getCurrentCommand(query, selection, force); if (command && command[0] && '@room'.startsWith(command[0]) && command[0].length > 1) { return [{ From d6a6e59d6c509cb34de2f72eb63f790fe1c12d01 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 6 Nov 2017 16:37:16 +0000 Subject: [PATCH 13/29] Indicate admins in the group member list with a sheriff badge --- .../views/groups/GroupMemberTile.js | 6 ++--- src/components/views/rooms/EntityTile.js | 27 ++++++++++++------- src/components/views/rooms/MemberTile.js | 8 +++++- src/groups.js | 1 + 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/components/views/groups/GroupMemberTile.js b/src/components/views/groups/GroupMemberTile.js index f40c7ed1c5..84c2adcb41 100644 --- a/src/components/views/groups/GroupMemberTile.js +++ b/src/components/views/groups/GroupMemberTile.js @@ -61,9 +61,9 @@ export default withMatrixClient(React.createClass({ ); return ( - ); }, diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index 1df19340cd..ffcb289437 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -47,7 +47,7 @@ function presenceClassForMember(presenceState, lastActiveAgo) { } } -module.exports = React.createClass({ +const EntityTile = React.createClass({ displayName: 'EntityTile', propTypes: { @@ -140,16 +140,19 @@ module.exports = React.createClass({ } let power; - const powerLevel = this.props.powerLevel; - if (powerLevel >= 50 && powerLevel < 99) { - power = {_t("Moderator")}; - } - if (powerLevel >= 99) { - power = {_t("Admin")}; + const powerStatus = this.props.powerStatus; + if (powerStatus) { + const src = { + [EntityTile.POWER_STATUS_MODERATOR]: "img/mod.svg", + [EntityTile.POWER_STATUS_ADMIN]: "img/admin.svg", + }[powerStatus]; + const alt = { + [EntityTile.POWER_STATUS_MODERATOR]: _t("Moderator"), + [EntityTile.POWER_STATUS_ADMIN]: _t("Admin"), + }[powerStatus]; + power = {alt}; } - - const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const av = this.props.avatarJsx || ; @@ -168,3 +171,9 @@ module.exports = React.createClass({ ); }, }); + +EntityTile.POWER_STATUS_MODERATOR = "moderator"; +EntityTile.POWER_STATUS_ADMIN = "admin"; + + +export default EntityTile; diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index e21f7c91f4..4848c4b258 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -86,13 +86,19 @@ module.exports = React.createClass({ } this.member_last_modified_time = member.getLastModifiedTime(); + // We deliberately leave power levels that are not 100 or 50 undefined + const powerStatus = { + 100: EntityTile.POWER_STATUS_ADMIN, + 50: EntityTile.POWER_STATUS_MODERATOR, + }[this.props.member.powerLevel]; + return ( + name={name} powerStatus={powerStatus} /> ); }, }); diff --git a/src/groups.js b/src/groups.js index 6c266e0fb6..957db1d85b 100644 --- a/src/groups.js +++ b/src/groups.js @@ -36,6 +36,7 @@ export function groupMemberFromApiObject(apiObject) { userId: apiObject.user_id, displayname: apiObject.displayname, avatarUrl: apiObject.avatar_url, + isAdmin: apiObject.is_admin, }; } From 08d006d1125f73cf96ad7bbbcb12cdb48e5af359 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 6 Nov 2017 17:15:09 +0000 Subject: [PATCH 14/29] PR feedback --- src/components/views/rooms/MessageComposerInput.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 42ab553b46..f6dfe90735 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -59,7 +59,9 @@ const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000; const ZWS_CODE = 8203; const ZWS = String.fromCharCode(ZWS_CODE); // zero width space -const ATROOMPILL_ENTITY_TYPE = 'ATROOMPILL'; +const ENTITY_TYPES = { + AT_ROOM_PILL = 'ATROOMPILL', +}; function stateToMarkdown(state) { return __stateToMarkdown(state) @@ -199,7 +201,7 @@ export default class MessageComposerInput extends React.Component { entityKey !== null && ( contentState.getEntity(entityKey).getType() === 'LINK' || - contentState.getEntity(entityKey).getType() === ATROOMPILL_ENTITY_TYPE + contentState.getEntity(entityKey).getType() === ENTITY_TYPES.AT_ROOM_PILL ) ); }, callback, @@ -221,7 +223,7 @@ export default class MessageComposerInput extends React.Component { const Pill = sdk.getComponent('elements.Pill'); const type = entityProps.contentState.getEntity(entityProps.entityKey).getType(); const {url} = entityProps.contentState.getEntity(entityProps.entityKey).getData(); - if (type === ATROOMPILL_ENTITY_TYPE) { + if (type === ENTITY_TYPES.AT_ROOM_PILL) { return Date: Mon, 6 Nov 2017 17:52:46 +0000 Subject: [PATCH 15/29] Ignore img tags in HTML if src is not specified This applies to HTML messages and group summaries. --- src/HtmlUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index b306eab23c..0c262fe89a 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -208,7 +208,7 @@ const sanitizeHtmlParams = { // Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag // because transformTags is used _before_ we filter by allowedSchemesByTag and // we don't want to allow images with `https?` `src`s. - if (!attribs.src.startsWith('mxc://')) { + if (!attribs.src || !attribs.src.startsWith('mxc://')) { return { tagName, attribs: {}}; } attribs.src = MatrixClientPeg.get().mxcUrlToHttp( From 36cd22663a8c4eb2a526b28f64105c4c14647636 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 6 Nov 2017 18:02:50 +0000 Subject: [PATCH 16/29] 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 17/29] 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)") }