From f032284eff219c279939740d65216cb49e50dfa8 Mon Sep 17 00:00:00 2001 From: "Andrew (anoa)" Date: Tue, 24 Oct 2017 16:21:46 -0700 Subject: [PATCH 01/31] 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 02/31] 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 03/31] 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 4953d4de4d60f83290d101bee0f0f453c3ed2b4c Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 2 Nov 2017 17:51:08 +0000 Subject: [PATCH 04/31] Give autocomplete providers the room they're in Removes the gut-wrenching that RoomView does to jam the user list into the user autocomplete provider. --- src/autocomplete/AutocompleteProvider.js | 3 + src/autocomplete/Autocompleter.js | 77 +++++++++++-------- src/autocomplete/CommandProvider.js | 8 -- src/autocomplete/DuckDuckGoProvider.js | 9 --- src/autocomplete/EmojiProvider.js | 7 -- src/autocomplete/RoomProvider.js | 10 --- src/autocomplete/UserProvider.js | 52 +++++++++---- src/components/structures/RoomView.js | 12 --- src/components/views/rooms/Autocomplete.js | 22 +++++- .../views/rooms/MessageComposerInput.js | 4 +- 10 files changed, 107 insertions(+), 97 deletions(-) diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index 4c7d039da4..ece833eb06 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.js @@ -28,6 +28,9 @@ export default class AutocompleteProvider { } } + destroy() { + } + /** * Of the matched commands in the query, returns the first that contains or is contained by the selection, or null. */ diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index 5b10110f04..ca3ef2a55a 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -45,41 +45,56 @@ const PROVIDERS = [ EmojiProvider, CommandProvider, DuckDuckGoProvider, -].map((completer) => completer.getInstance()); +]; // Providers will get rejected if they take longer than this. const PROVIDER_COMPLETION_TIMEOUT = 3000; -export async function getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array { - /* Note: That this waits for all providers to return is *intentional* - otherwise, we run into a condition where new completions are displayed - while the user is interacting with the list, which makes it difficult - to predict whether an action will actually do what is intended - */ - const completionsList = await Promise.all( - // Array of inspections of promises that might timeout. Instead of allowing a - // single timeout to reject the Promise.all, reflect each one and once they've all - // settled, filter for the fulfilled ones - PROVIDERS.map((provider) => { - return provider - .getCompletions(query, selection, force) - .timeout(PROVIDER_COMPLETION_TIMEOUT) - .reflect(); - }), - ); +export default class Autocompleter { + constructor(room) { + this.room = room; + this.providers = PROVIDERS.map((p) => { + return new p(room); + }); + } - return completionsList.filter( - (inspection) => inspection.isFulfilled(), - ).map((completionsState, i) => { - return { - completions: completionsState.value(), - provider: PROVIDERS[i], + destroy() { + this.providers.forEach((p) => { + p.destroy(); + }); + } - /* the currently matched "command" the completer tried to complete - * we pass this through so that Autocomplete can figure out when to - * re-show itself once hidden. - */ - command: PROVIDERS[i].getCurrentCommand(query, selection, force), - }; - }); + async getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array { + /* Note: That this waits for all providers to return is *intentional* + otherwise, we run into a condition where new completions are displayed + while the user is interacting with the list, which makes it difficult + to predict whether an action will actually do what is intended + */ + const completionsList = await Promise.all( + // Array of inspections of promises that might timeout. Instead of allowing a + // single timeout to reject the Promise.all, reflect each one and once they've all + // settled, filter for the fulfilled ones + this.providers.map((provider) => { + return provider + .getCompletions(query, selection, force) + .timeout(PROVIDER_COMPLETION_TIMEOUT) + .reflect(); + }), + ); + + return completionsList.filter( + (inspection) => inspection.isFulfilled(), + ).map((completionsState, i) => { + return { + completions: completionsState.value(), + provider: this.providers[i], + + /* the currently matched "command" the completer tried to complete + * we pass this through so that Autocomplete can figure out when to + * re-show itself once hidden. + */ + command: this.providers[i].getCurrentCommand(query, selection, force), + }; + }); + } } diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index e85457e6aa..df24a6b991 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -109,8 +109,6 @@ const COMMANDS = [ const COMMAND_RE = /(^\/\w*)/g; -let instance = null; - export default class CommandProvider extends AutocompleteProvider { constructor() { super(COMMAND_RE); @@ -142,12 +140,6 @@ export default class CommandProvider extends AutocompleteProvider { return '*️⃣ ' + _t('Commands'); } - static getInstance(): CommandProvider { - if (instance === null) instance = new CommandProvider(); - - return instance; - } - renderCompletions(completions: [React.Component]): ?React.Component { return
{ completions } diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index b2e85c4668..fdf260e1a1 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -25,8 +25,6 @@ import {TextualCompletion} from './Components'; const DDG_REGEX = /\/ddg\s+(.+)$/g; const REFERRER = 'vector'; -let instance = null; - export default class DuckDuckGoProvider extends AutocompleteProvider { constructor() { super(DDG_REGEX); @@ -96,13 +94,6 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { return '🔍 ' + _t('Results from DuckDuckGo'); } - static getInstance(): DuckDuckGoProvider { - if (instance == null) { - instance = new DuckDuckGoProvider(); - } - return instance; - } - renderCompletions(completions: [React.Component]): ?React.Component { return
{ completions } diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index a5b80e3b0e..eceaffeab4 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -70,8 +70,6 @@ const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sor }; }); -let instance = null; - function score(query, space) { const index = space.indexOf(query); if (index === -1) { @@ -151,11 +149,6 @@ export default class EmojiProvider extends AutocompleteProvider { return '😃 ' + _t('Emoji'); } - static getInstance() { - if (instance == null) {instance = new EmojiProvider();} - return instance; - } - renderCompletions(completions: [React.Component]): ?React.Component { return
{ completions } diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index cc04f54dda..11fd2618ac 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -27,8 +27,6 @@ import _sortBy from 'lodash/sortBy'; const ROOM_REGEX = /(?=#)(\S*)/g; -let instance = null; - function score(query, space) { const index = space.indexOf(query); if (index === -1) { @@ -96,14 +94,6 @@ export default class RoomProvider extends AutocompleteProvider { return '💬 ' + _t('Rooms'); } - static getInstance() { - if (instance == null) { - instance = new RoomProvider(); - } - - return instance; - } - renderCompletions(completions: [React.Component]): ?React.Component { return
{ completions } diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 296399c06c..8656de28aa 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -30,20 +30,54 @@ import type {Room, RoomMember} from 'matrix-js-sdk'; const USER_REGEX = /@\S*/g; -let instance = null; - export default class UserProvider extends AutocompleteProvider { users: Array = null; room: Room = null; - constructor() { + constructor(room) { super(USER_REGEX, { keys: ['name'], }); + this.room = room; this.matcher = new FuzzyMatcher([], { keys: ['name', 'userId'], shouldMatchPrefix: true, }); + + this._onRoomTimelineBound = this._onRoomTimeline.bind(this); + this._onRoomStateMemberBound = this._onRoomStateMember.bind(this); + + MatrixClientPeg.get().on("Room.timeline", this._onRoomTimelineBound); + MatrixClientPeg.get().on("RoomState.members", this._onRoomStateMemberBound); + } + + destroy() { + MatrixClientPeg.get().removeListener("Room.timeline", this._onRoomTimelineBound); + MatrixClientPeg.get().removeListener("RoomState.members", this._onRoomStateMemberBound); + } + + _onRoomTimeline(ev, room, toStartOfTimeline, removed, data) { + if (!room) return; + if (room.roomId != this.room.roomId) return; + + // ignore events from filtered timelines + if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return; + + // ignore anything but real-time updates at the end of the room: + // updates from pagination will happen when the paginate completes. + if (toStartOfTimeline || !data || !data.liveEvent) return; + + this.onUserSpoke(ev.sender); + } + + _onRoomStateMember(ev, state, member) { + // ignore members in other rooms + if (member.roomId !== this.room.roomId) { + return; + } + + // blow away the users cache + this.users = null; } async getCompletions(query: string, selection: {start: number, end: number}, force = false) { @@ -86,11 +120,6 @@ export default class UserProvider extends AutocompleteProvider { return '👥 ' + _t('Users'); } - setUserListFromRoom(room: Room) { - this.room = room; - this.users = null; - } - _makeUsers() { const events = this.room.getLiveTimeline().getEvents(); const lastSpoken = {}; @@ -123,13 +152,6 @@ export default class UserProvider extends AutocompleteProvider { this.matcher.setObjects(this.users); } - static getInstance(): UserProvider { - if (instance == null) { - instance = new UserProvider(); - } - return instance; - } - renderCompletions(completions: [React.Component]): ?React.Component { return
{ completions } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 9b6dbb4c27..a40fff274c 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -44,8 +44,6 @@ const Rooms = require('../../Rooms'); import KeyCode from '../../KeyCode'; -import UserProvider from '../../autocomplete/UserProvider'; - import RoomViewStore from '../../stores/RoomViewStore'; import RoomScrollStateStore from '../../stores/RoomScrollStateStore'; @@ -541,12 +539,6 @@ module.exports = React.createClass({ }); } } - - // update the tab complete list as it depends on who most recently spoke, - // and that has probably just changed - if (ev.sender) { - UserProvider.getInstance().onUserSpoke(ev.sender); - } }, onRoomName: function(room) { @@ -568,7 +560,6 @@ module.exports = React.createClass({ this._warnAboutEncryption(room); this._calculatePeekRules(room); this._updatePreviewUrlVisibility(room); - UserProvider.getInstance().setUserListFromRoom(room); }, _warnAboutEncryption: function(room) { @@ -722,9 +713,6 @@ module.exports = React.createClass({ // refresh the conf call notification state this._updateConfCallNotification(); - // refresh the tab complete list - UserProvider.getInstance().setUserListFromRoom(this.state.room); - // if we are now a member of the room, where we were not before, that // means we have finished joining a room we were previously peeking // into. diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index ecc908a02c..b877f388a8 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -1,5 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; import classNames from 'classnames'; import flatMap from 'lodash/flatMap'; import isEqual from 'lodash/isEqual'; @@ -8,7 +9,7 @@ import type {Completion} from '../../../autocomplete/Autocompleter'; import Promise from 'bluebird'; import UserSettingsStore from '../../../UserSettingsStore'; -import {getCompletions} from '../../../autocomplete/Autocompleter'; +import Autocompleter from '../../../autocomplete/Autocompleter'; const COMPOSER_SELECTED = 0; @@ -17,6 +18,7 @@ export default class Autocomplete extends React.Component { constructor(props) { super(props); + this.autocompleter = new Autocompleter(props.room); this.completionPromise = null; this.hide = this.hide.bind(this); this.onCompletionClicked = this.onCompletionClicked.bind(this); @@ -41,6 +43,11 @@ export default class Autocomplete extends React.Component { } componentWillReceiveProps(newProps, state) { + if (this.props.room.roomId !== newProps.room.roomId) { + this.autocompleter.destroy(); + this.autocompleter = new Autocompleter(); + } + // Query hasn't changed so don't try to complete it if (newProps.query === this.props.query) { return; @@ -49,6 +56,10 @@ export default class Autocomplete extends React.Component { this.complete(newProps.query, newProps.selection); } + componentWillUnmount() { + this.autocompleter.destroy(); + } + complete(query, selection) { this.queryRequested = query; if (this.debounceCompletionsRequest) { @@ -83,7 +94,7 @@ export default class Autocomplete extends React.Component { } processQuery(query, selection) { - return getCompletions( + return this.autocompleter.getCompletions( query, selection, this.state.forceComplete, ).then((completions) => { // Only ever process the completions for the most recent query being processed @@ -267,8 +278,11 @@ export default class Autocomplete extends React.Component { Autocomplete.propTypes = { // the query string for which to show autocomplete suggestions - query: React.PropTypes.string.isRequired, + query: PropTypes.string.isRequired, // method invoked with range and text content when completion is confirmed - onConfirm: React.PropTypes.func.isRequired, + onConfirm: PropTypes.func.isRequired, + + // The room in which we're autocompleting + room: PropTypes.object, }; diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 4850428621..45499eae04 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -1130,10 +1130,12 @@ export default class MessageComposerInput extends React.Component {
this.autocomplete = e} + room={this.props.room} onConfirm={this.setDisplayedCompletion} onSelectionChange={this.setDisplayedCompletion} query={this.getAutocompleteQuery(content)} - selection={selection} /> + selection={selection} + />
Date: Thu, 2 Nov 2017 18:01:28 +0000 Subject: [PATCH 05/31] copyrights --- src/autocomplete/AutocompleteProvider.js | 1 + src/autocomplete/Autocompleter.js | 3 +++ src/autocomplete/CommandProvider.js | 1 + src/autocomplete/DuckDuckGoProvider.js | 1 + src/autocomplete/EmojiProvider.js | 1 + src/autocomplete/RoomProvider.js | 1 + src/autocomplete/UserProvider.js | 1 + src/components/views/rooms/Autocomplete.js | 17 +++++++++++++++++ .../views/rooms/MessageComposerInput.js | 1 + 9 files changed, 27 insertions(+) diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index ece833eb06..0477e964bf 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.js @@ -1,6 +1,7 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd +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. diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index ca3ef2a55a..13b078cfa8 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -1,5 +1,6 @@ /* Copyright 2016 Aviral Dasgupta +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. @@ -22,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 = { @@ -43,6 +45,7 @@ const PROVIDERS = [ UserProvider, RoomProvider, EmojiProvider, + NotifProvider, CommandProvider, DuckDuckGoProvider, ]; diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index df24a6b991..d47f1a161a 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -1,6 +1,7 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd +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. diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index fdf260e1a1..68d4915f56 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -1,6 +1,7 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd +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. diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index eceaffeab4..9f1f40dbe7 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -1,6 +1,7 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd +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. diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index 11fd2618ac..1e1928a1ee 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -1,6 +1,7 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd +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. diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 8656de28aa..fced0ce7ff 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -2,6 +2,7 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd +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. diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index b877f388a8..839679f5c4 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -1,3 +1,20 @@ +/* +Copyright 2016 Aviral Dasgupta +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 ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 45499eae04..43f3aa5d88 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +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. From f7201e8dee0c81a8fb129fbd27adda9f60982fc6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 2 Nov 2017 18:08:24 +0000 Subject: [PATCH 06/31] Revert unintentional changes --- src/autocomplete/Autocompleter.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index 13b078cfa8..3d02765589 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -23,7 +23,6 @@ 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 = { @@ -45,7 +44,6 @@ const PROVIDERS = [ UserProvider, RoomProvider, EmojiProvider, - NotifProvider, CommandProvider, DuckDuckGoProvider, ]; From 42589281d12b0ef94705c5f739a5f73729d41f2d Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 2 Nov 2017 18:10:13 +0000 Subject: [PATCH 07/31] comment stub method --- src/autocomplete/AutocompleteProvider.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index 0477e964bf..c93ae4fb2a 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.js @@ -30,6 +30,7 @@ export default class AutocompleteProvider { } destroy() { + // stub } /** From ee43c635d1952ce4324595fa67ffa35b87a2f959 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 2 Nov 2017 18:11:18 +0000 Subject: [PATCH 08/31] phrasing --- src/autocomplete/Autocompleter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index 3d02765589..94d2ed28de 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -66,7 +66,7 @@ export default class Autocompleter { } async getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array { - /* Note: That this waits for all providers to return is *intentional* + /* Note: This intentionally waits for all providers to return, otherwise, we run into a condition where new completions are displayed while the user is interacting with the list, which makes it difficult to predict whether an action will actually do what is intended From 3b58f0ca2a00004e7423c79a7c876a6e294686c0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 2 Nov 2017 18:14:21 +0000 Subject: [PATCH 09/31] Ignore removed events --- src/autocomplete/UserProvider.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index fced0ce7ff..7c86ea7e4b 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -59,6 +59,7 @@ export default class UserProvider extends AutocompleteProvider { _onRoomTimeline(ev, room, toStartOfTimeline, removed, data) { if (!room) return; + if (removed) return; if (room.roomId != this.room.roomId) return; // ignore events from filtered timelines From 6ad4bb80dd366fe744f107b4cd5600e495d402ad Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 2 Nov 2017 18:14:46 +0000 Subject: [PATCH 10/31] == --- src/autocomplete/UserProvider.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 7c86ea7e4b..8b43964b1a 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -60,7 +60,7 @@ export default class UserProvider extends AutocompleteProvider { _onRoomTimeline(ev, room, toStartOfTimeline, removed, data) { if (!room) return; if (removed) return; - if (room.roomId != this.room.roomId) return; + if (room.roomId !== this.room.roomId) return; // ignore events from filtered timelines if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return; From 7f9967389d00e4bb40b5e43457cb059f90fb9201 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 2 Nov 2017 18:15:26 +0000 Subject: [PATCH 11/31] Pass room into Autocompleter --- src/components/views/rooms/Autocomplete.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 839679f5c4..db50ab8bf8 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -62,7 +62,7 @@ export default class Autocomplete extends React.Component { componentWillReceiveProps(newProps, state) { if (this.props.room.roomId !== newProps.room.roomId) { this.autocompleter.destroy(); - this.autocompleter = new Autocompleter(); + this.autocompleter = new Autocompleter(newProps.room); } // Query hasn't changed so don't try to complete it From 843d797ded2aa95d81d026a615bf9ac4a9d9ee50 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 2 Nov 2017 18:17:57 +0000 Subject: [PATCH 12/31] Better type checking --- src/components/views/rooms/Autocomplete.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index db50ab8bf8..958d16073c 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -25,6 +25,7 @@ import sdk from '../../../index'; import type {Completion} from '../../../autocomplete/Autocompleter'; import Promise from 'bluebird'; import UserSettingsStore from '../../../UserSettingsStore'; +import { Room } from 'matrix-js-sdk'; import Autocompleter from '../../../autocomplete/Autocompleter'; @@ -301,5 +302,5 @@ Autocomplete.propTypes = { onConfirm: PropTypes.func.isRequired, // The room in which we're autocompleting - room: PropTypes.object, + room: PropTypes.instanceOf(Room), }; From 112c74a255e9a74c8393dbc299b790e944f9482a Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 2 Nov 2017 18:54:25 +0000 Subject: [PATCH 13/31] 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 71c59eff2c439922bbc73b048d23f61023eb2275 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 3 Nov 2017 11:19:29 +0000 Subject: [PATCH 14/31] Add a GeminiScrollbar to Your Communities --- src/components/structures/MyGroups.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index b6a450fbb4..cc4783fdac 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -15,13 +15,14 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; +import GeminiScrollbar from 'react-gemini-scrollbar'; import {MatrixClient} from 'matrix-js-sdk'; import sdk from '../../index'; import { _t, _tJsx } from '../../languageHandler'; import withMatrixClient from '../../wrappers/withMatrixClient'; import AccessibleButton from '../views/elements/AccessibleButton'; import dis from '../../dispatcher'; -import PropTypes from 'prop-types'; import Modal from '../../Modal'; import FlairStore from '../../stores/FlairStore'; @@ -115,18 +116,17 @@ export default withMatrixClient(React.createClass({ const TintableSvg = sdk.getComponent("elements.TintableSvg"); let content; + let contentHeader; if (this.state.groups) { const groupNodes = []; this.state.groups.forEach((g) => { groupNodes.push(); }); + contentHeader = groupNodes.length > 0 ?

{ _t('Your Communities') }

:
; content = groupNodes.length > 0 ? -
-

{ _t('Your Communities') }

-
- { groupNodes } -
-
: + + { groupNodes } + :
{ _t( "You're not currently a member of any communities.", @@ -176,6 +176,7 @@ export default withMatrixClient(React.createClass({
+ { contentHeader } { content }
; From 151f9917b1232ec8658aef5821db3d59c5b01212 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 3 Nov 2017 12:19:37 +0000 Subject: [PATCH 15/31] Fix group invites such that they look similar to room invites - Change GroupInviteTile to use RoomTile CSS - Give group invites their own sub list, with heading "Community Invites" --- src/components/views/groups/GroupInviteTile.js | 12 ++++++------ src/components/views/rooms/RoomList.js | 15 ++++++++++++--- src/i18n/strings/en_EN.json | 1 + 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js index d7a04247ec..fcc9acb00b 100644 --- a/src/components/views/groups/GroupInviteTile.js +++ b/src/components/views/groups/GroupInviteTile.js @@ -44,21 +44,21 @@ export default React.createClass({ const label = { groupName } ; - const badge =
!
; + const badge =
!
; return ( - -
+ +
{ av }
-
+
{ label } { badge }
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index e689579650..1a9fa5d4e9 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -555,13 +555,23 @@ module.exports = React.createClass({ render: function() { const RoomSubList = sdk.getComponent('structures.RoomSubList'); - const inviteSectionExtraTiles = this._makeGroupInviteTiles(); - const self = this; return (
+ + to start a chat with someone": "Press to start a chat with someone", "You're not in any rooms yet! Press to make a room or to browse the directory": "You're not in any rooms yet! Press to make a room or to browse the directory", + "Community Invites": "Community Invites", "Invites": "Invites", "Favourites": "Favourites", "People": "People", 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 16/31] 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 802ab1674660c864edda59c0aa2aa8d4d6574e67 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 3 Nov 2017 14:06:59 +0000 Subject: [PATCH 17/31] Fix multiple pills on one line --- src/components/views/messages/TextualBody.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index faa4d6cf77..911f2c98d1 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -194,6 +194,9 @@ module.exports = React.createClass({ node.parentNode.replaceChild(pillContainer, node); // Pills within pills aren't going to go well, so move on pillified = true; + + // update the current node with one that's now taken its place + node = pillContainer; } } else if (node.nodeType == Node.TEXT_NODE) { const Pill = sdk.getComponent('elements.Pill'); From e84e4ed5f5804639493d04ddb695ebd9adcca18c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 3 Nov 2017 15:38:12 +0000 Subject: [PATCH 18/31] Add CSS classes to group ID input in CreateGroupDialog --- src/components/views/dialogs/CreateGroupDialog.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index 12f419ddd6..38d61cfc11 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -137,16 +137,18 @@ export default React.createClass({
-
- + - + + + - :{ MatrixClientPeg.get().getDomain() } + + :{ MatrixClientPeg.get().getDomain() } +
From b88c13d5274b878db07a9177f6a6bba5d9fda29c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 3 Nov 2017 16:02:41 +0000 Subject: [PATCH 19/31] Use prefixed class names to avoid collisions with other libraries --- src/components/views/dialogs/CreateGroupDialog.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index 38d61cfc11..e1dfe388d6 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -138,15 +138,16 @@ export default React.createClass({
- + - + + - + :{ MatrixClientPeg.get().getDomain() }
From 98312a464762aa3e984c76e5ef611f1e307c0d9d Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 3 Nov 2017 17:37:13 +0000 Subject: [PATCH 20/31] Use, if possible, a room's canonical or first alias when viewing the room (Only affects rooms the user has never joined) --- src/components/views/rooms/RoomDetailList.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/RoomDetailList.js b/src/components/views/rooms/RoomDetailList.js index 5374094f1f..69f3faa325 100644 --- a/src/components/views/rooms/RoomDetailList.js +++ b/src/components/views/rooms/RoomDetailList.js @@ -49,6 +49,7 @@ const RoomDetailRow = React.createClass({ dis.dispatch({ action: 'view_room', room_id: this.props.room.roomId, + room_alias: this.props.room.canonicalAlias || this.props.room.aliases[0], }); }, From 58f1757f0cd350d213f6bc3e68a8900810f052b2 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 3 Nov 2017 17:52:36 +0000 Subject: [PATCH 21/31] Handle rooms with no aliases or canonical alias --- src/components/views/rooms/RoomDetailList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomDetailList.js b/src/components/views/rooms/RoomDetailList.js index 69f3faa325..c44b662fa7 100644 --- a/src/components/views/rooms/RoomDetailList.js +++ b/src/components/views/rooms/RoomDetailList.js @@ -49,7 +49,7 @@ const RoomDetailRow = React.createClass({ dis.dispatch({ action: 'view_room', room_id: this.props.room.roomId, - room_alias: this.props.room.canonicalAlias || this.props.room.aliases[0], + room_alias: this.props.room.canonicalAlias || (this.props.room.aliases || [])[0], }); }, From d64fc4c842ca993b10b9a7e764d481c4c5fbe4ed Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 3 Nov 2017 18:19:10 +0000 Subject: [PATCH 22/31] Fix Flair not appearing due to missing this._usersInFlight --- src/stores/FlairStore.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/stores/FlairStore.js b/src/stores/FlairStore.js index 9424503390..411065eee0 100644 --- a/src/stores/FlairStore.js +++ b/src/stores/FlairStore.js @@ -48,6 +48,9 @@ class FlairStore extends EventEmitter { // reject: () => {} // } }; + this._usersInFlight = { + // see _usersPending + }; this._debounceTimeoutID = null; } From 4d8f18742b5dc1563f58867604e215a7685df411 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 3 Nov 2017 18:43:43 +0000 Subject: [PATCH 23/31] Check against non-existant promise to resolve a user's groups --- src/stores/FlairStore.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/stores/FlairStore.js b/src/stores/FlairStore.js index 411065eee0..587890da7f 100644 --- a/src/stores/FlairStore.js +++ b/src/stores/FlairStore.js @@ -128,12 +128,16 @@ class FlairStore extends EventEmitter { } catch (err) { // Propagate the same error to all usersInFlight Object.keys(this._usersInFlight).forEach((userId) => { + // The promise should always exist for userId, but do a null-check anyway + if (!this._usersInFlight[userId]) return; this._usersInFlight[userId].reject(err); }); return; } const updatedUserGroups = resp.users; Object.keys(this._usersInFlight).forEach((userId) => { + // The promise should always exist for userId, but do a null-check anyway + if (!this._usersInFlight[userId]) return; this._usersInFlight[userId].resolve(updatedUserGroups[userId] || []); }); } From da23afdec228fff34c24dd8b923f8b0490078e7b Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 3 Nov 2017 18:48:15 +0000 Subject: [PATCH 24/31] Better comment on FlairStore _usersInFlight --- src/stores/FlairStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/FlairStore.js b/src/stores/FlairStore.js index 587890da7f..1d20148c0d 100644 --- a/src/stores/FlairStore.js +++ b/src/stores/FlairStore.js @@ -49,7 +49,7 @@ class FlairStore extends EventEmitter { // } }; this._usersInFlight = { - // see _usersPending + // This has the same schema as _usersPending }; this._debounceTimeoutID = null; From febeb0429eb9c582bf24cf60a14a26053c509b24 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 6 Nov 2017 10:18:10 +0000 Subject: [PATCH 25/31] 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 26/31] 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 27/31] 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 28/31] 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 29/31] 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 30/31] 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 df5c6470e84865c09e80f25d7b14009c8d4cf937 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 6 Nov 2017 22:01:23 +0000 Subject: [PATCH 31/31] get dict syntax right --- src/components/views/rooms/MessageComposerInput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index f6dfe90735..aa019de091 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -60,7 +60,7 @@ const ZWS_CODE = 8203; const ZWS = String.fromCharCode(ZWS_CODE); // zero width space const ENTITY_TYPES = { - AT_ROOM_PILL = 'ATROOMPILL', + AT_ROOM_PILL: 'ATROOMPILL', }; function stateToMarkdown(state) {