From 7bd6bb6eb652c4c88a8f2d004383ab7a789260f9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 7 Nov 2018 13:19:29 +0100 Subject: [PATCH 01/93] make MatrixDispatcher constructor public so we can create one for each open room --- src/dispatcher.js | 36 ++------------------------- src/matrix-dispatcher.js | 53 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 34 deletions(-) create mode 100644 src/matrix-dispatcher.js diff --git a/src/dispatcher.js b/src/dispatcher.js index 48c8dc86e9..4dc6e1e37d 100644 --- a/src/dispatcher.js +++ b/src/dispatcher.js @@ -17,42 +17,10 @@ limitations under the License. 'use strict'; -const flux = require("flux"); - -class MatrixDispatcher extends flux.Dispatcher { - /** - * @param {Object|function} payload Required. The payload to dispatch. - * If an Object, must contain at least an 'action' key. - * If a function, must have the signature (dispatch) => {...}. - * @param {boolean=} sync Optional. Pass true to dispatch - * synchronously. This is useful for anything triggering - * an operation that the browser requires user interaction - * for. - */ - dispatch(payload, sync) { - // Allow for asynchronous dispatching by accepting payloads that have the - // type `function (dispatch) {...}` - if (typeof payload === 'function') { - payload((action) => { - this.dispatch(action, sync); - }); - return; - } - - if (sync) { - super.dispatch(payload); - } else { - // Unless the caller explicitly asked for us to dispatch synchronously, - // we always set a timeout to do this: The flux dispatcher complains - // if you dispatch from within a dispatch, so rather than action - // handlers having to worry about not calling anything that might - // then dispatch, we just do dispatches asynchronously. - setTimeout(super.dispatch.bind(this, payload), 0); - } - } -} +import MatrixDispatcher from "./matrix-dispatcher"; if (global.mxDispatcher === undefined) { global.mxDispatcher = new MatrixDispatcher(); } + module.exports = global.mxDispatcher; diff --git a/src/matrix-dispatcher.js b/src/matrix-dispatcher.js new file mode 100644 index 0000000000..fb81ed837f --- /dev/null +++ b/src/matrix-dispatcher.js @@ -0,0 +1,53 @@ +/* +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. +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. +*/ + +'use strict'; + +const flux = require("flux"); + +export default class MatrixDispatcher extends flux.Dispatcher { + /** + * @param {Object|function} payload Required. The payload to dispatch. + * If an Object, must contain at least an 'action' key. + * If a function, must have the signature (dispatch) => {...}. + * @param {boolean=} sync Optional. Pass true to dispatch + * synchronously. This is useful for anything triggering + * an operation that the browser requires user interaction + * for. + */ + dispatch(payload, sync) { + // Allow for asynchronous dispatching by accepting payloads that have the + // type `function (dispatch) {...}` + if (typeof payload === 'function') { + payload((action) => { + this.dispatch(action, sync); + }); + return; + } + + if (sync) { + super.dispatch(payload); + } else { + // Unless the caller explicitly asked for us to dispatch synchronously, + // we always set a timeout to do this: The flux dispatcher complains + // if you dispatch from within a dispatch, so rather than action + // handlers having to worry about not calling anything that might + // then dispatch, we just do dispatches asynchronously. + setTimeout(super.dispatch.bind(this, payload), 0); + } + } +} From 869c81eb9059900d2a6ac1d99aab5a4acd407253 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 7 Nov 2018 13:21:14 +0100 Subject: [PATCH 02/93] cram OpenRoomsStore between RoomViewStore and dispatcher the idea is that it will keep a RoomViewStore for every room on the screen, and also keep track of which one is the current one. For now, it just replicates the existing functionality of having just 1 room on the screen. Since the RoomViewStore just has access to a local dispatcher and not the global anymore, all dispatching of actions needs to be moved to the OpenRoomsStore, so room alias resolving, event forwarding, ... is moved there. --- src/stores/OpenRoomsStore.js | 166 +++++++++++++++++++++++++++++++++++ src/stores/RoomViewStore.js | 45 ++-------- 2 files changed, 174 insertions(+), 37 deletions(-) create mode 100644 src/stores/OpenRoomsStore.js diff --git a/src/stores/OpenRoomsStore.js b/src/stores/OpenRoomsStore.js new file mode 100644 index 0000000000..f82671e58d --- /dev/null +++ b/src/stores/OpenRoomsStore.js @@ -0,0 +1,166 @@ +/* +Copyright 2018 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 MatrixDispatcher from '../matrix-dispatcher'; +import dis from '../dispatcher'; +import {RoomViewStore} from './RoomViewStore'; +import {Store} from 'flux/utils'; +import MatrixClientPeg from '../MatrixClientPeg'; + +/** + * A class for keeping track of the RoomViewStores of the rooms shown on the screen. + * Routes the dispatcher actions to the store of currently active room. + */ +class OpenRoomsStore extends Store { + constructor() { + super(dis); + + // Initialise state + this._state = { + room: { + store: null, + dispatcher: null + }, + }; + + this._forwardingEvent = null; + } + + getRoomStore() { + return this._state.room.store; + } + + getCurrentRoomStore() { + return this.getRoomStore(); // just one room for now + } + + _setState(newState) { + this._state = Object.assign(this._state, newState); + this.__emitChange(); + } + + _cleanupRoom() { + const room = this._state.room; + room.dispatcher.unregister(room.store.getDispatchToken()); + this._setState({ + room: { + store: null, + dispatcher: null + }, + }); + } + + _createRoom() { + const dispatcher = new MatrixDispatcher(); + this._setState({ + room: { + store: new RoomViewStore(dispatcher), + dispatcher, + }, + }); + } + + _forwardAction(payload) { + if (this._state.room.dispatcher) { + this._state.room.dispatcher.dispatch(payload, true); + } + } + + async _resolveRoomAlias(payload) { + try { + const result = await MatrixClientPeg.get() + .getRoomIdForAlias(payload.room_alias); + dis.dispatch({ + action: 'view_room', + room_id: result.room_id, + event_id: payload.event_id, + highlighted: payload.highlighted, + room_alias: payload.room_alias, + auto_join: payload.auto_join, + oob_data: payload.oob_data, + }); + } catch(err) { + this._forwardAction({ + action: 'view_room_error', + room_id: null, + room_alias: payload.room_alias, + err: err, + }); + } + } + + __onDispatch(payload) { + switch (payload.action) { + // view_room: + // - room_alias: '#somealias:matrix.org' + // - room_id: '!roomid123:matrix.org' + // - event_id: '$213456782:matrix.org' + // - event_offset: 100 + // - highlighted: true + case 'view_room': + console.log("!!! OpenRoomsStore: view_room", payload); + if (!payload.room_id && payload.room_alias) { + this._resolveRoomAlias(payload); + } + const currentStore = this.getCurrentRoomStore(); + if (currentStore && + (!payload.room_alias || payload.room_alias !== currentStore.getRoomAlias()) && + (!currentStore.getRoomId() || payload.room_id !== currentStore.getRoomId()) + ) { + console.log("OpenRoomsStore: _cleanupRoom"); + this._cleanupRoom(); + } + if (!this._state.room.store) { + console.log("OpenRoomsStore: _createRoom"); + this._createRoom(); + } + console.log("OpenRoomsStore: _forwardAction"); + this._forwardAction(payload); + if (this._forwardingEvent) { + dis.dispatch({ + action: 'send_event', + room_id: payload.room_id, + event: this._forwardingEvent, + }); + this._forwardingEvent = null; + } + break; + case 'view_my_groups': + case 'view_group': + this._forwardAction(payload); + this._cleanupRoom(); + break; + case 'will_join': + case 'cancel_join': + case 'join_room': + case 'join_room_error': + case 'on_logged_out': + case 'reply_to_event': + case 'open_room_settings': + case 'close_settings': + this._forwardAction(payload); + break; + case 'forward_event': + this._forwardingEvent = payload.event; + break; + } + } +} + +let singletonOpenRoomsStore = null; +if (!singletonOpenRoomsStore) { + singletonOpenRoomsStore = new OpenRoomsStore(); +} +module.exports = singletonOpenRoomsStore; diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index f15925f480..43964bc6c3 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -53,8 +53,8 @@ const INITIAL_STATE = { * with a subset of the js-sdk. * ``` */ -class RoomViewStore extends Store { - constructor() { +export class RoomViewStore extends Store { + constructor(dis) { super(dis); // Initialise state @@ -85,6 +85,8 @@ class RoomViewStore extends Store { }); break; case 'view_room_error': + // should not go over dispatcher anymore + // but be internal to RoomViewStore this._viewRoomError(payload); break; case 'will_join': @@ -150,22 +152,11 @@ class RoomViewStore extends Store { // pull the user out of Room Settings isEditingSettings: false, }; - - if (this._state.forwardingEvent) { - dis.dispatch({ - action: 'send_event', - room_id: newState.roomId, - event: this._state.forwardingEvent, - }); - } - this._setState(newState); - if (payload.auto_join) { this._joinRoom(payload); } } else if (payload.room_alias) { - // Resolve the alias and then do a second dispatch with the room ID acquired this._setState({ roomId: null, initialEventId: null, @@ -175,25 +166,6 @@ class RoomViewStore extends Store { roomLoading: true, roomLoadError: null, }); - MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias).done( - (result) => { - dis.dispatch({ - action: 'view_room', - room_id: result.room_id, - event_id: payload.event_id, - highlighted: payload.highlighted, - room_alias: payload.room_alias, - auto_join: payload.auto_join, - oob_data: payload.oob_data, - }); - }, (err) => { - dis.dispatch({ - action: 'view_room_error', - room_id: null, - room_alias: payload.room_alias, - err: err, - }); - }); } } @@ -330,8 +302,7 @@ class RoomViewStore extends Store { } } -let singletonRoomViewStore = null; -if (!singletonRoomViewStore) { - singletonRoomViewStore = new RoomViewStore(); -} -module.exports = singletonRoomViewStore; +const MatrixDispatcher = require("../matrix-dispatcher"); +const blubber = new RoomViewStore(new MatrixDispatcher()); + +export default blubber; From df8539d6bc59c0857fdaf8613da891558bd70445 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 7 Nov 2018 13:24:35 +0100 Subject: [PATCH 03/93] pass the RoomViewStore down with a prop instead of global var. this will allow to have more than 1 RoomView further on --- src/components/structures/LoggedInView.js | 1 + src/components/structures/RightPanel.js | 2 +- src/components/structures/RoomView.js | 33 ++++++++++--------- src/components/views/rooms/MemberInfo.js | 4 +-- src/components/views/rooms/MessageComposer.js | 11 ++++--- .../views/rooms/MessageComposerInput.js | 6 ++-- src/components/views/rooms/ReplyPreview.js | 5 ++- 7 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 635c5de44e..b81597a901 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -429,6 +429,7 @@ const LoggedInView = React.createClass({ switch (this.props.page_type) { case PageTypes.RoomView: page_element = ; } else if (this.state.phase === RightPanel.Phase.RoomMemberInfo) { - panel = ; + panel = ; } else if (this.state.phase === RightPanel.Phase.GroupMemberInfo) { panel = { try { await cli.invite(roomId, member.userId); diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index dc927f0e0a..5ac788fb1d 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -22,7 +22,6 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; import sdk from '../../../index'; import dis from '../../../dispatcher'; -import RoomViewStore from '../../../stores/RoomViewStore'; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; import Stickerpicker from './Stickerpicker'; import { makeRoomPermalink } from '../../../matrix-to'; @@ -63,7 +62,7 @@ export default class MessageComposer extends React.Component { isRichTextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'), }, showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'), - isQuoting: Boolean(RoomViewStore.getQuotingEvent()), + isQuoting: Boolean(this.props.roomViewStore.getQuotingEvent()), tombstone: this._getRoomTombstone(), }; } @@ -75,7 +74,7 @@ export default class MessageComposer extends React.Component { // XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something. MatrixClientPeg.get().on("event", this.onEvent); MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents); - this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); + this._roomStoreToken = this.props.roomViewStore.addListener(this._onRoomViewStoreUpdate); this._waitForOwnMember(); } @@ -124,7 +123,7 @@ export default class MessageComposer extends React.Component { } _onRoomViewStoreUpdate() { - const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); + const isQuoting = Boolean(this.props.roomViewStore.getQuotingEvent()); if (this.state.isQuoting === isQuoting) return; this.setState({ isQuoting }); } @@ -153,7 +152,7 @@ export default class MessageComposer extends React.Component { ); } - const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); + const isQuoting = Boolean(this.props.roomViewStore.getQuotingEvent()); let replyToWarning = null; if (isQuoting) { replyToWarning =

{ @@ -357,6 +356,7 @@ export default class MessageComposer extends React.Component { controls.push( this.messageComposerInput = c} key="controls_input" onResize={this.props.onResize} @@ -461,4 +461,5 @@ MessageComposer.propTypes = { // string representing the current room app drawer state showApps: PropTypes.bool, + roomViewStore: PropTypes.object.isRequired, }; diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 14d394ab41..da41dba212 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -58,7 +58,6 @@ import {asciiRegexp, unicodeRegexp, shortnameToUnicode, emojioneList, asciiList, import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; import {makeUserPermalink} from "../../../matrix-to"; import ReplyPreview from "./ReplyPreview"; -import RoomViewStore from '../../../stores/RoomViewStore'; import ReplyThread from "../elements/ReplyThread"; import {ContentHelpers} from 'matrix-js-sdk'; @@ -150,6 +149,7 @@ export default class MessageComposerInput extends React.Component { onFilesPasted: PropTypes.func, onInputStateChanged: PropTypes.func, + roomViewStore: PropTypes.object.isRequired, }; client: MatrixClient; @@ -1120,7 +1120,7 @@ export default class MessageComposerInput extends React.Component { return true; } - const replyingToEv = RoomViewStore.getQuotingEvent(); + const replyingToEv = this.props.roomViewStore.getQuotingEvent(); const mustSendHTML = Boolean(replyingToEv); if (this.state.isRichTextEnabled) { @@ -1589,7 +1589,7 @@ export default class MessageComposerInput extends React.Component { return (

- + this.autocomplete = e} room={this.props.room} diff --git a/src/components/views/rooms/ReplyPreview.js b/src/components/views/rooms/ReplyPreview.js index 46e2826634..04ff9d0778 100644 --- a/src/components/views/rooms/ReplyPreview.js +++ b/src/components/views/rooms/ReplyPreview.js @@ -18,7 +18,6 @@ import React from 'react'; import dis from '../../../dispatcher'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; -import RoomViewStore from '../../../stores/RoomViewStore'; import SettingsStore from "../../../settings/SettingsStore"; function cancelQuoting() { @@ -38,7 +37,7 @@ export default class ReplyPreview extends React.Component { this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this); - this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); + this._roomStoreToken = this.props.roomViewStore.addListener(this._onRoomViewStoreUpdate); this._onRoomViewStoreUpdate(); } @@ -50,7 +49,7 @@ export default class ReplyPreview extends React.Component { } _onRoomViewStoreUpdate() { - const event = RoomViewStore.getQuotingEvent(); + const event = this.props.roomViewStore.getQuotingEvent(); if (this.state.event !== event) { this.setState({ event }); } From 43efa29ef8d284da1286867fb48525166196bd52 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 7 Nov 2018 13:25:42 +0100 Subject: [PATCH 04/93] track active room with OpenRoomsStore --- src/ActiveRoomObserver.js | 15 ++++++++------- src/components/views/rooms/RoomTile.js | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/ActiveRoomObserver.js b/src/ActiveRoomObserver.js index d6fbb460b5..ee3212f611 100644 --- a/src/ActiveRoomObserver.js +++ b/src/ActiveRoomObserver.js @@ -14,10 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import RoomViewStore from './stores/RoomViewStore'; +import OpenRoomsStore from './stores/OpenRoomsStore'; /** - * Consumes changes from the RoomViewStore and notifies specific things + * Consumes changes from the OpenRoomsStore and notifies specific things * about when the active room changes. Unlike listening for RoomViewStore * changes, you can subscribe to only changes relevant to a particular * room. @@ -28,11 +28,11 @@ import RoomViewStore from './stores/RoomViewStore'; class ActiveRoomObserver { constructor() { this._listeners = {}; - - this._activeRoomId = RoomViewStore.getRoomId(); + const roomStore = OpenRoomsStore.getCurrentRoomStore(); + this._activeRoomId = roomStore && roomStore.getRoomId(); // TODO: We could self-destruct when the last listener goes away, or at least // stop listening. - this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate.bind(this)); + this._roomStoreToken = OpenRoomsStore.addListener(this._onOpenRoomsStoreUpdate.bind(this)); } addListener(roomId, listener) { @@ -59,12 +59,13 @@ class ActiveRoomObserver { } } - _onRoomViewStoreUpdate() { + _onOpenRoomsStoreUpdate() { // emit for the old room ID if (this._activeRoomId) this._emit(this._activeRoomId); + const activeRoomStore = OpenRoomsStore.getCurrentRoomStore(); // update our cache - this._activeRoomId = RoomViewStore.getRoomId(); + this._activeRoomId = activeRoomStore && activeRoomStore.getRoomId(); // and emit for the new one if (this._activeRoomId) this._emit(this._activeRoomId); diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index faa08c7001..71be2df5a4 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -116,9 +116,9 @@ module.exports = React.createClass({ } }, - _onActiveRoomChange: function() { + _onActiveRoomChange: function(activeRoomId) { this.setState({ - selected: this.props.room.roomId === RoomViewStore.getRoomId(), + selected: this.props.room.roomId === activeRoomId, }); }, From 78d5d7ac0c55e0b6c50a0a56ec170795d0ef8dd9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 7 Nov 2018 16:28:58 +0100 Subject: [PATCH 05/93] correctly detected collapsed rhs --- src/components/structures/MainSplit.js | 2 +- src/components/structures/RoomGridView.js | 92 +++++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/components/structures/RoomGridView.js diff --git a/src/components/structures/MainSplit.js b/src/components/structures/MainSplit.js index 6fd0274f1a..5c69ef6745 100644 --- a/src/components/structures/MainSplit.js +++ b/src/components/structures/MainSplit.js @@ -55,7 +55,7 @@ export default class MainSplit extends React.Component { } componentDidMount() { - if (this.props.panel && !this.collapsedRhs) { + if (this.props.panel && !this.props.collapsedRhs) { this._createResizer(); } } diff --git a/src/components/structures/RoomGridView.js b/src/components/structures/RoomGridView.js new file mode 100644 index 0000000000..d472767dbb --- /dev/null +++ b/src/components/structures/RoomGridView.js @@ -0,0 +1,92 @@ +/* +Copyright 2017 Vector Creations Ltd. +Copyright 2017, 2018 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 PropTypes from 'prop-types'; + +export default class RoomGridView extends React.Component { + /* displayName: 'GroupView', + + propTypes: { + groupId: PropTypes.string.isRequired, + }, + + childContextTypes = { + groupStore: PropTypes.instanceOf(GroupStore), + };*/ + + getInitialState() { + return { + rooms: [], + }; + } + + componentWillMount() { + this._unmounted = false; + this._initGroupStore(this.props.groupId); + this._dispatcherRef = dis.register(this._onAction); + } + + componentWillUnmount() { + this._unmounted = true; + if (this._groupStoreRegistration) { + this._groupStoreRegistration.unregister(); + } + dis.unregister(this._dispatcherRef); + } + + componentWillReceiveProps(newProps) { + if (this.props.groupId != newProps.groupId) { + this.setState(this.getInitialState(), () => { + this._initGroupStore(newProps.groupId); + }); + } + } + + _initGroupStore(groupId) { + if (this._groupStoreRegistration) { + this._groupStoreRegistration.unregister(); + } + this._groupStoreRegistration = GroupStore.registerListener(groupId, this.onGroupStoreUpdated); + } + + onGroupStoreUpdated() { + if (this._unmounted) return; + this.setState({ + rooms: GroupStore.getGroupRooms(this.props.groupId), + }); + } + + _onAction(payload) { + switch (payload.action) { + default: + break; + } + } + + render() { + const rooms = this.state.rooms.slice(0, 6); + return
+ { rooms.map(room => { +
+ +
+ }) } +
+ } + +} From 720bc11aa403e98650cf3952232518c9628bd3d5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 7 Nov 2018 16:29:17 +0100 Subject: [PATCH 06/93] avoid using roomviewstore for detecting selected room --- src/ActiveRoomObserver.js | 4 ++++ src/components/views/rooms/RoomTile.js | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ActiveRoomObserver.js b/src/ActiveRoomObserver.js index ee3212f611..2561a2e6a3 100644 --- a/src/ActiveRoomObserver.js +++ b/src/ActiveRoomObserver.js @@ -35,6 +35,10 @@ class ActiveRoomObserver { this._roomStoreToken = OpenRoomsStore.addListener(this._onOpenRoomsStoreUpdate.bind(this)); } + getActiveRoomId() { + return this._activeRoomId; + } + addListener(roomId, listener) { if (!this._listeners[roomId]) this._listeners[roomId] = []; this._listeners[roomId].push(listener); diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 71be2df5a4..2bc06ecc7a 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -29,7 +29,6 @@ import * as RoomNotifs from '../../../RoomNotifs'; import * as FormattingUtils from '../../../utils/FormattingUtils'; import AccessibleButton from '../elements/AccessibleButton'; import ActiveRoomObserver from '../../../ActiveRoomObserver'; -import RoomViewStore from '../../../stores/RoomViewStore'; module.exports = React.createClass({ displayName: 'RoomTile', @@ -61,7 +60,7 @@ module.exports = React.createClass({ roomName: this.props.room.name, notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId), notificationCount: this.props.room.getUnreadNotificationCount(), - selected: this.props.room.roomId === RoomViewStore.getRoomId(), + selected: this.props.room.roomId === ActiveRoomObserver.getActiveRoomId(), }); }, From f95b26179fbc558c1200f6e5c3e8476a5969084a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 7 Nov 2018 16:30:02 +0100 Subject: [PATCH 07/93] make copy of initial state, as there can be multiple instances now --- src/stores/RoomViewStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 43964bc6c3..5ec0c67f2d 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -58,7 +58,7 @@ export class RoomViewStore extends Store { super(dis); // Initialise state - this._state = INITIAL_STATE; + this._state = Object.assign({}, INITIAL_STATE); } _setState(newState) { From d7924ad1a8e4876ab8f016f535c7b91165326a08 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 7 Nov 2018 16:30:29 +0100 Subject: [PATCH 08/93] less ambigious name for local dispatcher --- src/stores/RoomViewStore.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 5ec0c67f2d..af1264921a 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -54,8 +54,8 @@ const INITIAL_STATE = { * ``` */ export class RoomViewStore extends Store { - constructor(dis) { - super(dis); + constructor(dispatcher) { + super(dispatcher); // Initialise state this._state = Object.assign({}, INITIAL_STATE); From 6ec6303b973c0fd51928e61591472784a85563b3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 7 Nov 2018 16:32:12 +0100 Subject: [PATCH 09/93] support opening all rooms of a group in OpenRoomsStore using new view_group_grid action --- src/stores/OpenRoomsStore.js | 112 +++++++++++++++++++++++++++-------- 1 file changed, 87 insertions(+), 25 deletions(-) diff --git a/src/stores/OpenRoomsStore.js b/src/stores/OpenRoomsStore.js index f82671e58d..6904689157 100644 --- a/src/stores/OpenRoomsStore.js +++ b/src/stores/OpenRoomsStore.js @@ -16,9 +16,21 @@ limitations under the License. import MatrixDispatcher from '../matrix-dispatcher'; import dis from '../dispatcher'; import {RoomViewStore} from './RoomViewStore'; +import GroupStore from './GroupStore'; import {Store} from 'flux/utils'; import MatrixClientPeg from '../MatrixClientPeg'; + +function matchesRoom(payload, roomStore) { + if (!roomStore) { + return false; + } + if (payload.room_alias) { + return payload.room_alias === roomStore.getRoomAlias(); + } + return payload.room_id === roomStore.getRoomId(); +} + /** * A class for keeping track of the RoomViewStores of the rooms shown on the screen. * Routes the dispatcher actions to the store of currently active room. @@ -29,21 +41,30 @@ class OpenRoomsStore extends Store { // Initialise state this._state = { - room: { - store: null, - dispatcher: null - }, + rooms: [], + currentIndex: null, + group_id: null, }; this._forwardingEvent = null; } - getRoomStore() { - return this._state.room.store; + getRoomStores() { + return this._state.rooms.map((r) => r.store); } getCurrentRoomStore() { - return this.getRoomStore(); // just one room for now + const currentRoom = this._getCurrentRoom(); + if (currentRoom) { + return currentRoom.store; + } + } + + _getCurrentRoom() { + const index = this._state.currentIndex; + if (index !== null && index < this._state.rooms.length) { + return this._state.rooms[index]; + } } _setState(newState) { @@ -51,30 +72,41 @@ class OpenRoomsStore extends Store { this.__emitChange(); } - _cleanupRoom() { + _hasRoom(payload) { + return this._roomIndex(payload) !== -1; + } + + _roomIndex(payload) { + return this._state.rooms.findIndex((r) => matchesRoom(payload, r.store)); + } + + _cleanupRooms() { const room = this._state.room; - room.dispatcher.unregister(room.store.getDispatchToken()); + this._state.rooms.forEach((room) => { + room.dispatcher.unregister(room.store.getDispatchToken()); + }); this._setState({ - room: { - store: null, - dispatcher: null - }, + rooms: [], + group_id: null, + currentIndex: null }); } _createRoom() { const dispatcher = new MatrixDispatcher(); this._setState({ - room: { + rooms: [{ store: new RoomViewStore(dispatcher), dispatcher, - }, + }], + currentIndex: 0, }); } _forwardAction(payload) { - if (this._state.room.dispatcher) { - this._state.room.dispatcher.dispatch(payload, true); + const currentRoom = this._getCurrentRoom(); + if (currentRoom) { + currentRoom.dispatcher.dispatch(payload, true); } } @@ -101,6 +133,10 @@ class OpenRoomsStore extends Store { } } + _setCurrentGroupRoom(index) { + this._setState({currentIndex: index}); + } + __onDispatch(payload) { switch (payload.action) { // view_room: @@ -115,14 +151,15 @@ class OpenRoomsStore extends Store { this._resolveRoomAlias(payload); } const currentStore = this.getCurrentRoomStore(); - if (currentStore && - (!payload.room_alias || payload.room_alias !== currentStore.getRoomAlias()) && - (!currentStore.getRoomId() || payload.room_id !== currentStore.getRoomId()) - ) { - console.log("OpenRoomsStore: _cleanupRoom"); - this._cleanupRoom(); + if (matchesRoom(payload, currentStore)) { + if (this._hasRoom(payload)) { + const roomIndex = this._roomIndex(payload); + this._setState({currentIndex: roomIndex}); + } else { + this._cleanupRooms(); + } } - if (!this._state.room.store) { + if (!this.getCurrentRoomStore()) { console.log("OpenRoomsStore: _createRoom"); this._createRoom(); } @@ -140,7 +177,7 @@ class OpenRoomsStore extends Store { case 'view_my_groups': case 'view_group': this._forwardAction(payload); - this._cleanupRoom(); + this._cleanupRooms(); break; case 'will_join': case 'cancel_join': @@ -155,6 +192,31 @@ class OpenRoomsStore extends Store { case 'forward_event': this._forwardingEvent = payload.event; break; + case 'view_group_grid': + if (payload.group_id !== this._state.group_id) { + this._cleanupRooms(); + // TODO: register to GroupStore updates + const rooms = GroupStore.getGroupRooms(payload.group_id); + const roomStores = rooms.map((room) => { + const dispatcher = new MatrixDispatcher(); + const store = new RoomViewStore(dispatcher); + // set room id of store + dispatcher.dispatch({ + action: 'view_room', + room_id: room.roomId + }, true); + return { + store, + dispatcher, + }; + }); + this._setState({ + rooms: roomStores, + group_id: payload.group_id, + }); + this._setCurrentGroupRoom(0); + } + break; } } } From d4748c91df73a6b1e96daf9c46afe4446966d55e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 7 Nov 2018 16:33:14 +0100 Subject: [PATCH 10/93] add first draft of RoomGridView --- res/css/_components.scss | 1 + res/css/structures/_GroupGridView.scss | 65 +++++++++++++++ src/components/structures/GroupGridView.js | 84 ++++++++++++++++++++ src/components/structures/RoomGridView.js | 92 ---------------------- 4 files changed, 150 insertions(+), 92 deletions(-) create mode 100644 res/css/structures/_GroupGridView.scss create mode 100644 src/components/structures/GroupGridView.js delete mode 100644 src/components/structures/RoomGridView.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 92e243e8d1..16bb4938c1 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -5,6 +5,7 @@ @import "./structures/_ContextualMenu.scss"; @import "./structures/_CreateRoom.scss"; @import "./structures/_FilePanel.scss"; +@import "./structures/_GroupGridView.scss"; @import "./structures/_GroupView.scss"; @import "./structures/_HomePage.scss"; @import "./structures/_LeftPanel.scss"; diff --git a/res/css/structures/_GroupGridView.scss b/res/css/structures/_GroupGridView.scss new file mode 100644 index 0000000000..ed0d824388 --- /dev/null +++ b/res/css/structures/_GroupGridView.scss @@ -0,0 +1,65 @@ +/* +Copyright 2017 Vector Creations 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. +*/ + +.mx_GroupGridView { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr; + grid-column-gap: 10px; + grid-row-gap: 10px; + background-color: red; +} + +.mx_RoomGridView_emptyTile::before { + display: block; + margin-top: 100px; + text-align: center; + content: "no room in this tile yet"; +} + +.mx_RoomGridView_tile > .mx_RoomView { + height: 100%; +} + +.mx_GroupGridView > *:nth-child(1) { + grid-column: 1; + grid-row: 1; +} + +.mx_GroupGridView > *:nth-child(2) { + grid-column: 2; + grid-row: 1; +} + +.mx_GroupGridView > *:nth-child(3) { + grid-column: 3; + grid-row: 1; +} + +.mx_GroupGridView > *:nth-child(4) { + grid-column: 1; + grid-row: 2; +} + +.mx_GroupGridView > *:nth-child(5) { + grid-column: 2; + grid-row: 2; +} + +.mx_GroupGridView > *:nth-child(6) { + grid-column: 3; + grid-row: 2; +} diff --git a/src/components/structures/GroupGridView.js b/src/components/structures/GroupGridView.js new file mode 100644 index 0000000000..282919f019 --- /dev/null +++ b/src/components/structures/GroupGridView.js @@ -0,0 +1,84 @@ +/* +Copyright 2017 Vector Creations Ltd. +Copyright 2017, 2018 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 PropTypes from 'prop-types'; +import OpenRoomsStore from '../../stores/OpenRoomsStore'; +import dis from '../../dispatcher'; +import RoomView from './RoomView'; + +export default class RoomGridView extends React.Component { + + constructor(props) { + super(props); + this.state = { + roomStores: OpenRoomsStore.getRoomStores(), + }; + } + + componentWillMount() { + this._unmounted = false; + this._openRoomsStoreRegistration = OpenRoomsStore.addListener(this.onRoomsChanged); + this._dispatcherRef = dis.register(this._onAction); + } + + componentWillUnmount() { + this._unmounted = true; + if (this._openRoomsStoreRegistration) { + this._openRoomsStoreRegistration.unregister(); + } + dis.unregister(this._dispatcherRef); + } + + onRoomsChanged() { + if (this._unmounted) return; + this.setState({ + roomStores: OpenRoomsStore.getRoomStores(), + }); + } + + _onAction(payload) { + switch (payload.action) { + default: + break; + } + } + + render() { + let roomStores = this.state.roomStores.slice(0, 6); + const emptyCount = 6 - roomStores.length; + if (emptyCount) { + const emptyTiles = Array.from({length: emptyCount}, () => null); + roomStores = roomStores.concat(emptyTiles); + } + return (
+ { roomStores.map(roomStore => { + if (roomStore) { + return (
+ +
); + } else { + return (
); + } + }) } +
); + } + +} diff --git a/src/components/structures/RoomGridView.js b/src/components/structures/RoomGridView.js deleted file mode 100644 index d472767dbb..0000000000 --- a/src/components/structures/RoomGridView.js +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd. -Copyright 2017, 2018 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 PropTypes from 'prop-types'; - -export default class RoomGridView extends React.Component { - /* displayName: 'GroupView', - - propTypes: { - groupId: PropTypes.string.isRequired, - }, - - childContextTypes = { - groupStore: PropTypes.instanceOf(GroupStore), - };*/ - - getInitialState() { - return { - rooms: [], - }; - } - - componentWillMount() { - this._unmounted = false; - this._initGroupStore(this.props.groupId); - this._dispatcherRef = dis.register(this._onAction); - } - - componentWillUnmount() { - this._unmounted = true; - if (this._groupStoreRegistration) { - this._groupStoreRegistration.unregister(); - } - dis.unregister(this._dispatcherRef); - } - - componentWillReceiveProps(newProps) { - if (this.props.groupId != newProps.groupId) { - this.setState(this.getInitialState(), () => { - this._initGroupStore(newProps.groupId); - }); - } - } - - _initGroupStore(groupId) { - if (this._groupStoreRegistration) { - this._groupStoreRegistration.unregister(); - } - this._groupStoreRegistration = GroupStore.registerListener(groupId, this.onGroupStoreUpdated); - } - - onGroupStoreUpdated() { - if (this._unmounted) return; - this.setState({ - rooms: GroupStore.getGroupRooms(this.props.groupId), - }); - } - - _onAction(payload) { - switch (payload.action) { - default: - break; - } - } - - render() { - const rooms = this.state.rooms.slice(0, 6); - return
- { rooms.map(room => { -
- -
- }) } -
- } - -} From 399d3c5c24da0f8f7ac5c1e47464f14560375b3a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 7 Nov 2018 16:34:10 +0100 Subject: [PATCH 11/93] wire up view_group_grid action from community context menu to new view --- src/PageTypes.js | 1 + src/components/structures/LoggedInView.js | 12 +++++++++++- src/components/structures/MatrixChat.js | 10 ++++++++++ .../views/context_menus/TagTileContextMenu.js | 12 ++++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/PageTypes.js b/src/PageTypes.js index 60111723fb..e4e1916c8b 100644 --- a/src/PageTypes.js +++ b/src/PageTypes.js @@ -19,6 +19,7 @@ limitations under the License. export default { HomePage: "home_page", RoomView: "room_view", + GroupGridView: "group_grid_view", UserSettings: "user_settings", RoomDirectory: "room_directory", UserView: "user_view", diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index b81597a901..8c6f192016 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -31,6 +31,7 @@ import sessionStore from '../../stores/SessionStore'; import MatrixClientPeg from '../../MatrixClientPeg'; import SettingsStore from "../../settings/SettingsStore"; import RoomListStore from "../../stores/RoomListStore"; +import OpenRoomsStore from "../../stores/OpenRoomsStore"; import TagOrderActions from '../../actions/TagOrderActions'; import RoomListActions from '../../actions/RoomListActions'; @@ -416,6 +417,7 @@ const LoggedInView = React.createClass({ const RoomDirectory = sdk.getComponent('structures.RoomDirectory'); const HomePage = sdk.getComponent('structures.HomePage'); const GroupView = sdk.getComponent('structures.GroupView'); + const GroupGridView = sdk.getComponent('structures.GroupGridView'); const MyGroups = sdk.getComponent('structures.MyGroups'); const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar'); const CookieBar = sdk.getComponent('globals.CookieBar'); @@ -428,6 +430,12 @@ const LoggedInView = React.createClass({ switch (this.props.page_type) { case PageTypes.RoomView: + if (!OpenRoomsStore.getCurrentRoomStore()) { + console.warn(`LoggedInView: getCurrentRoomStore not set!`); + } + else if (OpenRoomsStore.getCurrentRoomStore().getRoomId() !== this.props.currentRoomId) { + console.warn(`LoggedInView: room id in store not the same as in props: ${OpenRoomsStore.getCurrentRoomStore().getRoomId()} & ${this.props.currentRoomId}`); + } page_element = ; break; - + case PageTypes.GroupGridView: + page_element = ; + break; case PageTypes.UserSettings: page_element = @@ -65,6 +74,9 @@ export default class TagTileContextMenu extends React.Component { /> { _t('View Community') }
+
+ { _t('View as grid') } +

From fdd324a9437793bbdfb66c4e228cfca9f95f3c4d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 8 Nov 2018 11:06:26 +0100 Subject: [PATCH 12/93] basic divider lines for tiles --- res/css/structures/_GroupGridView.scss | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/res/css/structures/_GroupGridView.scss b/res/css/structures/_GroupGridView.scss index ed0d824388..4c54cb49b1 100644 --- a/res/css/structures/_GroupGridView.scss +++ b/res/css/structures/_GroupGridView.scss @@ -18,8 +18,6 @@ limitations under the License. display: grid; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr 1fr; - grid-column-gap: 10px; - grid-row-gap: 10px; background-color: red; } @@ -30,6 +28,11 @@ limitations under the License. content: "no room in this tile yet"; } +.mx_RoomGridView_tile { + border-right: 1px solid $panel-divider-color; + border-bottom: 1px solid $panel-divider-color; +} + .mx_RoomGridView_tile > .mx_RoomView { height: 100%; } From b68df0420b29769aacb4f2790da16ea8a01c1efe Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 8 Nov 2018 12:25:36 +0100 Subject: [PATCH 13/93] fix errors when trying to switch room --- src/components/structures/GroupGridView.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/GroupGridView.js b/src/components/structures/GroupGridView.js index 282919f019..b72e689bc5 100644 --- a/src/components/structures/GroupGridView.js +++ b/src/components/structures/GroupGridView.js @@ -28,6 +28,7 @@ export default class RoomGridView extends React.Component { this.state = { roomStores: OpenRoomsStore.getRoomStores(), }; + this.onRoomsChanged = this.onRoomsChanged.bind(this); } componentWillMount() { @@ -39,7 +40,7 @@ export default class RoomGridView extends React.Component { componentWillUnmount() { this._unmounted = true; if (this._openRoomsStoreRegistration) { - this._openRoomsStoreRegistration.unregister(); + this._openRoomsStoreRegistration.remove(); } dis.unregister(this._dispatcherRef); } From cf0f75cad4eaf7050a8cc106f74ce3c3c0b8f147 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 21 Nov 2018 14:04:25 +0000 Subject: [PATCH 14/93] allow changing active room in grid by clicking it --- res/css/structures/_GroupGridView.scss | 11 ++++++++--- src/components/structures/GroupGridView.js | 13 ++++++++++--- src/components/structures/MatrixChat.js | 2 +- src/components/structures/RoomView.js | 6 +++++- .../views/context_menus/TagTileContextMenu.js | 2 +- src/stores/OpenRoomsStore.js | 16 ++++++++++------ 6 files changed, 35 insertions(+), 15 deletions(-) diff --git a/res/css/structures/_GroupGridView.scss b/res/css/structures/_GroupGridView.scss index 4c54cb49b1..130d3e89f6 100644 --- a/res/css/structures/_GroupGridView.scss +++ b/res/css/structures/_GroupGridView.scss @@ -21,19 +21,24 @@ limitations under the License. background-color: red; } -.mx_RoomGridView_emptyTile::before { + +.mx_GroupGridView_emptyTile::before { display: block; margin-top: 100px; text-align: center; content: "no room in this tile yet"; } -.mx_RoomGridView_tile { +.mx_GroupGridView_tile { border-right: 1px solid $panel-divider-color; border-bottom: 1px solid $panel-divider-color; } -.mx_RoomGridView_tile > .mx_RoomView { +.mx_GroupGridView_activeTile { + border: 1px solid red !important; +} + +.mx_GroupGridView_tile > .mx_RoomView { height: 100%; } diff --git a/src/components/structures/GroupGridView.js b/src/components/structures/GroupGridView.js index b72e689bc5..09bc0e300a 100644 --- a/src/components/structures/GroupGridView.js +++ b/src/components/structures/GroupGridView.js @@ -20,6 +20,7 @@ import PropTypes from 'prop-types'; import OpenRoomsStore from '../../stores/OpenRoomsStore'; import dis from '../../dispatcher'; import RoomView from './RoomView'; +import classNames from 'classnames'; export default class RoomGridView extends React.Component { @@ -49,6 +50,7 @@ export default class RoomGridView extends React.Component { if (this._unmounted) return; this.setState({ roomStores: OpenRoomsStore.getRoomStores(), + currentRoomStore: OpenRoomsStore.getCurrentRoomStore(), }); } @@ -67,16 +69,21 @@ export default class RoomGridView extends React.Component { roomStores = roomStores.concat(emptyTiles); } return (
- { roomStores.map(roomStore => { + { roomStores.map((roomStore) => { if (roomStore) { - return (
+ const isActive = roomStore === this.state.currentRoomStore; + const tileClasses = classNames({ + "mx_GroupGridView_tile": true, + "mx_GroupGridView_activeTile": isActive, + }); + return (
); } else { - return (
); + return (
); } }) }
); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index c42daf2923..3d3aa52e57 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -627,7 +627,7 @@ export default React.createClass({ case 'view_group': this._viewGroup(payload); break; - case 'view_group_grid': + case 'group_grid_view': this._viewGroupGrid(payload); break; case 'view_home_page': diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index b1730ed96a..c7a0677262 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1470,6 +1470,10 @@ module.exports = React.createClass({ } }, + _onMainClicked: function() { + dis.dispatch({action: 'group_grid_set_active', room_id: this.state.room.roomId }); + }, + render: function() { const RoomHeader = sdk.getComponent('rooms.RoomHeader'); const MessageComposer = sdk.getComponent('rooms.MessageComposer'); @@ -1817,7 +1821,7 @@ module.exports = React.createClass({ const rightPanel = this.state.room ? : undefined; return ( -
+
Date: Thu, 22 Nov 2018 10:18:11 +0000 Subject: [PATCH 15/93] also give empty tiles a key --- src/components/structures/GroupGridView.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/GroupGridView.js b/src/components/structures/GroupGridView.js index 09bc0e300a..7a51604f0d 100644 --- a/src/components/structures/GroupGridView.js +++ b/src/components/structures/GroupGridView.js @@ -69,7 +69,7 @@ export default class RoomGridView extends React.Component { roomStores = roomStores.concat(emptyTiles); } return (
- { roomStores.map((roomStore) => { + { roomStores.map((roomStore, i) => { if (roomStore) { const isActive = roomStore === this.state.currentRoomStore; const tileClasses = classNames({ @@ -83,7 +83,7 @@ export default class RoomGridView extends React.Component { /> ); } else { - return (
); + return (
); } }) }
); From 2ceef0094437b3c3269d341c373138af964b2a70 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Nov 2018 11:01:19 +0000 Subject: [PATCH 16/93] style active room rect, and make it not jump --- res/css/structures/_GroupGridView.scss | 27 +++++++++++++++++++++++++- res/css/structures/_MatrixChat.scss | 3 ++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/res/css/structures/_GroupGridView.scss b/res/css/structures/_GroupGridView.scss index 130d3e89f6..9e5c5c3379 100644 --- a/res/css/structures/_GroupGridView.scss +++ b/res/css/structures/_GroupGridView.scss @@ -35,9 +35,34 @@ limitations under the License. } .mx_GroupGridView_activeTile { - border: 1px solid red !important; + position: relative; } +.mx_GroupGridView_activeTile:before, +.mx_GroupGridView_activeTile:after { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + content: ""; + pointer-events: none; + z-index: 10000; +} + +.mx_GroupGridView_activeTile:before { + border-radius: 14px; + border: 8px solid rgba(134, 193, 165, 0.5); + margin: -8px; +} + +.mx_GroupGridView_activeTile:after { + border-radius: 8px; + border: 2px solid rgba(134, 193, 165, 1); + margin: -2px; +} + + .mx_GroupGridView_tile > .mx_RoomView { height: 100%; } diff --git a/res/css/structures/_MatrixChat.scss b/res/css/structures/_MatrixChat.scss index 1ccbd19391..a843bb7fee 100644 --- a/res/css/structures/_MatrixChat.scss +++ b/res/css/structures/_MatrixChat.scss @@ -80,7 +80,8 @@ limitations under the License. Empirically this stops the MessagePanel's width exploding outwards when gemini is in 'prevented' mode */ - overflow-x: auto; + // disabling this for now as it clips the active room rect on the grid view + // overflow-x: auto; /* To fix https://github.com/vector-im/riot-web/issues/3298 where Safari needed height 100% all the way down to the HomePage. Height does not From fbfbefe4fe76b2344b568dbc2d747bf3da74ecae Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Nov 2018 12:23:07 +0000 Subject: [PATCH 17/93] also forward actions from room dispatcher to global one avoiding replay if the action would be forwarded back to the same room dispatcher also some fixing & renaming in OpenRoomsStore --- src/ActiveRoomObserver.js | 4 +- src/components/structures/GroupGridView.js | 4 +- src/components/structures/LoggedInView.js | 8 +- src/stores/OpenRoomsStore.js | 161 +++++++++++++-------- 4 files changed, 105 insertions(+), 72 deletions(-) diff --git a/src/ActiveRoomObserver.js b/src/ActiveRoomObserver.js index 2561a2e6a3..f3850dd87c 100644 --- a/src/ActiveRoomObserver.js +++ b/src/ActiveRoomObserver.js @@ -28,7 +28,7 @@ import OpenRoomsStore from './stores/OpenRoomsStore'; class ActiveRoomObserver { constructor() { this._listeners = {}; - const roomStore = OpenRoomsStore.getCurrentRoomStore(); + const roomStore = OpenRoomsStore.getActiveRoomStore(); this._activeRoomId = roomStore && roomStore.getRoomId(); // TODO: We could self-destruct when the last listener goes away, or at least // stop listening. @@ -67,7 +67,7 @@ class ActiveRoomObserver { // emit for the old room ID if (this._activeRoomId) this._emit(this._activeRoomId); - const activeRoomStore = OpenRoomsStore.getCurrentRoomStore(); + const activeRoomStore = OpenRoomsStore.getActiveRoomStore(); // update our cache this._activeRoomId = activeRoomStore && activeRoomStore.getRoomId(); diff --git a/src/components/structures/GroupGridView.js b/src/components/structures/GroupGridView.js index 7a51604f0d..e6c0dfd31e 100644 --- a/src/components/structures/GroupGridView.js +++ b/src/components/structures/GroupGridView.js @@ -50,7 +50,7 @@ export default class RoomGridView extends React.Component { if (this._unmounted) return; this.setState({ roomStores: OpenRoomsStore.getRoomStores(), - currentRoomStore: OpenRoomsStore.getCurrentRoomStore(), + activeRoomStore: OpenRoomsStore.getActiveRoomStore(), }); } @@ -71,7 +71,7 @@ export default class RoomGridView extends React.Component { return (
{ roomStores.map((roomStore, i) => { if (roomStore) { - const isActive = roomStore === this.state.currentRoomStore; + const isActive = roomStore === this.state.activeRoomStore; const tileClasses = classNames({ "mx_GroupGridView_tile": true, "mx_GroupGridView_activeTile": isActive, diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 8c6f192016..56f3fc89f4 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -430,14 +430,14 @@ const LoggedInView = React.createClass({ switch (this.props.page_type) { case PageTypes.RoomView: - if (!OpenRoomsStore.getCurrentRoomStore()) { + if (!OpenRoomsStore.getActiveRoomStore()) { console.warn(`LoggedInView: getCurrentRoomStore not set!`); } - else if (OpenRoomsStore.getCurrentRoomStore().getRoomId() !== this.props.currentRoomId) { - console.warn(`LoggedInView: room id in store not the same as in props: ${OpenRoomsStore.getCurrentRoomStore().getRoomId()} & ${this.props.currentRoomId}`); + else if (OpenRoomsStore.getActiveRoomStore().getRoomId() !== this.props.currentRoomId) { + console.warn(`LoggedInView: room id in store not the same as in props: ${OpenRoomsStore.getActiveRoomStore().getRoomId()} & ${this.props.currentRoomId}`); } page_element = r.store); } - getCurrentRoomStore() { - const currentRoom = this._getCurrentRoom(); - if (currentRoom) { - return currentRoom.store; + getActiveRoomStore() { + const openRoom = this._getActiveOpenRoom(); + if (openRoom) { + return openRoom.store; } } - _getCurrentRoom() { + _getActiveOpenRoom() { const index = this._state.currentIndex; if (index !== null && index < this._state.rooms.length) { return this._state.rooms[index]; @@ -80,31 +80,79 @@ class OpenRoomsStore extends Store { return this._state.rooms.findIndex((r) => matchesRoom(payload, r.store)); } - _cleanupRooms() { - const room = this._state.room; + _cleanupOpenRooms() { this._state.rooms.forEach((room) => { + room.dispatcher.unregister(room.dispatcherRef); room.dispatcher.unregister(room.store.getDispatchToken()); }); this._setState({ rooms: [], group_id: null, - currentIndex: null + currentIndex: null, }); } - _createRoom() { + _createOpenRoom(room_id, room_alias) { const dispatcher = new MatrixDispatcher(); + // forward all actions coming from the room dispatcher + // to the global one + const dispatcherRef = dispatcher.register((action) => { + action.grid_src_room_id = room_id; + action.grid_src_room_alias = room_alias; + this.getDispatcher().dispatch(action); + }); + const openRoom = { + store: new RoomViewStore(dispatcher), + dispatcher, + dispatcherRef, + }; + + dispatcher.dispatch({ + action: 'view_room', + room_id: room_id, + room_alias: room_alias, + }, true); + + return openRoom; + } + + _setSingleOpenRoom(payload) { this._setState({ - rooms: [{ - store: new RoomViewStore(dispatcher), - dispatcher, - }], + rooms: [this._createOpenRoom(payload.room_id, payload.room_alias)], + currentIndex: 0, + }); + } + + _setGroupOpenRooms(group_id) { + this._cleanupOpenRooms(); + // TODO: register to GroupStore updates + const rooms = GroupStore.getGroupRooms(group_id); + const openRooms = rooms.map((room) => { + return this._createOpenRoom(room.roomId); + }); + this._setState({ + rooms: openRooms, + group_id: group_id, currentIndex: 0, }); } _forwardAction(payload) { - const currentRoom = this._getCurrentRoom(); + // don't forward an event to a room dispatcher + // if the event originated from that dispatcher, as this + // would cause the event to be observed twice in that + // dispatcher + if (payload.grid_src_room_id || payload.grid_src_room_alias) { + const srcPayload = { + room_id: payload.grid_src_room_id, + room_alias: payload.grid_src_room_alias, + }; + const srcIndex = this._roomIndex(srcPayload); + if (srcIndex === this._state.currentIndex) { + return; + } + } + const currentRoom = this._getActiveOpenRoom(); if (currentRoom) { currentRoom.dispatcher.dispatch(payload, true); } @@ -114,7 +162,7 @@ class OpenRoomsStore extends Store { try { const result = await MatrixClientPeg.get() .getRoomIdForAlias(payload.room_alias); - dis.dispatch({ + this.getDispatcher().dispatch({ action: 'view_room', room_id: result.room_id, event_id: payload.event_id, @@ -133,6 +181,36 @@ class OpenRoomsStore extends Store { } } + _viewRoom(payload) { + console.log("!!! OpenRoomsStore: view_room", payload); + if (!payload.room_id && payload.room_alias) { + this._resolveRoomAlias(payload); + } + const currentStore = this.getActiveRoomStore(); + if (!matchesRoom(payload, currentStore)) { + if (this._hasRoom(payload)) { + const roomIndex = this._roomIndex(payload); + this._setState({currentIndex: roomIndex}); + } else { + this._cleanupOpenRooms(); + } + } + if (!this.getActiveRoomStore()) { + console.log("OpenRoomsStore: _setSingleOpenRoom"); + this._setSingleOpenRoom(payload); + } + console.log("OpenRoomsStore: _forwardAction"); + this._forwardAction(payload); + if (this._forwardingEvent) { + this.getDispatcher().dispatch({ + action: 'send_event', + room_id: payload.room_id, + event: this._forwardingEvent, + }); + this._forwardingEvent = null; + } + } + __onDispatch(payload) { switch (payload.action) { // view_room: @@ -142,38 +220,12 @@ class OpenRoomsStore extends Store { // - event_offset: 100 // - highlighted: true case 'view_room': - console.log("!!! OpenRoomsStore: view_room", payload); - if (!payload.room_id && payload.room_alias) { - this._resolveRoomAlias(payload); - } - const currentStore = this.getCurrentRoomStore(); - if (matchesRoom(payload, currentStore)) { - if (this._hasRoom(payload)) { - const roomIndex = this._roomIndex(payload); - this._setState({currentIndex: roomIndex}); - } else { - this._cleanupRooms(); - } - } - if (!this.getCurrentRoomStore()) { - console.log("OpenRoomsStore: _createRoom"); - this._createRoom(); - } - console.log("OpenRoomsStore: _forwardAction"); - this._forwardAction(payload); - if (this._forwardingEvent) { - dis.dispatch({ - action: 'send_event', - room_id: payload.room_id, - event: this._forwardingEvent, - }); - this._forwardingEvent = null; - } + this._viewRoom(payload); break; case 'view_my_groups': case 'view_group': this._forwardAction(payload); - this._cleanupRooms(); + this._cleanupOpenRooms(); break; case 'will_join': case 'cancel_join': @@ -183,6 +235,7 @@ class OpenRoomsStore extends Store { case 'reply_to_event': case 'open_room_settings': case 'close_settings': + case 'focus_composer': this._forwardAction(payload); break; case 'forward_event': @@ -198,27 +251,7 @@ class OpenRoomsStore extends Store { break; case 'group_grid_view': if (payload.group_id !== this._state.group_id) { - this._cleanupRooms(); - // TODO: register to GroupStore updates - const rooms = GroupStore.getGroupRooms(payload.group_id); - const roomStores = rooms.map((room) => { - const dispatcher = new MatrixDispatcher(); - const store = new RoomViewStore(dispatcher); - // set room id of store - dispatcher.dispatch({ - action: 'view_room', - room_id: room.roomId - }, true); - return { - store, - dispatcher, - }; - }); - this._setState({ - rooms: roomStores, - group_id: payload.group_id, - currentIndex: 0, - }); + this._setGroupOpenRooms(payload.group_id); } break; } From 9a24249fb5805bdbf9c623d2b5cbe5997ef0d85a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Nov 2018 15:13:03 +0000 Subject: [PATCH 18/93] emit focus_composer after updating the active room in GroupGridView also change the active room from there so RoomView is oblivious to grid view stuff --- src/components/structures/GroupGridView.js | 25 +++++++++++++++++++++- src/components/structures/RoomView.js | 6 +----- src/stores/OpenRoomsStore.js | 6 ++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/components/structures/GroupGridView.js b/src/components/structures/GroupGridView.js index e6c0dfd31e..0181966d49 100644 --- a/src/components/structures/GroupGridView.js +++ b/src/components/structures/GroupGridView.js @@ -28,10 +28,22 @@ export default class RoomGridView extends React.Component { super(props); this.state = { roomStores: OpenRoomsStore.getRoomStores(), + activeRoomStore: OpenRoomsStore.getActiveRoomStore(), }; this.onRoomsChanged = this.onRoomsChanged.bind(this); } + componentDidUpdate(_, prevState) { + const store = this.state.activeRoomStore; + if (store) { + store.getDispatcher().dispatch({action: 'focus_composer'}); + } + } + + componentDidMount() { + this.componentDidUpdate(); + } + componentWillMount() { this._unmounted = false; this._openRoomsStoreRegistration = OpenRoomsStore.addListener(this.onRoomsChanged); @@ -61,6 +73,16 @@ export default class RoomGridView extends React.Component { } } + _setActive(i) { + const store = OpenRoomsStore.getRoomStoreAt(i); + if (store !== this.state.activeRoomStore) { + dis.dispatch({ + action: 'group_grid_set_active', + room_id: store.getRoomId(), + }); + } + } + render() { let roomStores = this.state.roomStores.slice(0, 6); const emptyCount = 6 - roomStores.length; @@ -76,10 +98,11 @@ export default class RoomGridView extends React.Component { "mx_GroupGridView_tile": true, "mx_GroupGridView_activeTile": isActive, }); - return (
+ return (
{this._setActive(i)}} key={roomStore.getRoomId()} className={tileClasses}>
); } else { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index c7a0677262..b1730ed96a 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1470,10 +1470,6 @@ module.exports = React.createClass({ } }, - _onMainClicked: function() { - dis.dispatch({action: 'group_grid_set_active', room_id: this.state.room.roomId }); - }, - render: function() { const RoomHeader = sdk.getComponent('rooms.RoomHeader'); const MessageComposer = sdk.getComponent('rooms.MessageComposer'); @@ -1821,7 +1817,7 @@ module.exports = React.createClass({ const rightPanel = this.state.room ? : undefined; return ( -
+
= 0 && index < this._state.rooms.length) { + return this._state.rooms[index].store; + } + } + _getActiveOpenRoom() { const index = this._state.currentIndex; if (index !== null && index < this._state.rooms.length) { From 44200a6f78b025041e8a6a090e145d611bf1d312 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Nov 2018 15:15:24 +0000 Subject: [PATCH 19/93] only listen and dispatch to room-local dispatcher in room view, composer --- src/components/structures/RoomView.js | 59 +++++++++---------- src/components/views/rooms/MessageComposer.js | 10 ++-- .../views/rooms/MessageComposerInput.js | 12 ++-- 3 files changed, 39 insertions(+), 42 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index b1730ed96a..f6b9381080 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -35,7 +35,6 @@ const ContentMessages = require("../../ContentMessages"); const Modal = require("../../Modal"); const sdk = require('../../index'); const CallHandler = require('../../CallHandler'); -const dis = require("../../dispatcher"); const Tinter = require("../../Tinter"); const rate_limited_func = require('../../ratelimitedfunc'); const ObjectUtils = require('../../ObjectUtils'); @@ -151,7 +150,7 @@ module.exports = React.createClass({ }, componentWillMount: function() { - this.dispatcherRef = dis.register(this.onAction); + this.dispatcherRef = this.props.roomViewStore.getDispatcher().register(this.onAction); MatrixClientPeg.get().on("Room", this.onRoom); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().on("Room.name", this.onRoomName); @@ -200,7 +199,7 @@ module.exports = React.createClass({ editingRoomSettings: store.isEditingSettings(), }; - if (this.state.editingRoomSettings && !newState.editingRoomSettings) dis.dispatch({action: 'focus_composer'}); + if (this.state.editingRoomSettings && !newState.editingRoomSettings) this.props.roomViewStore.getDispatcher().dispatch({action: 'focus_composer'}); // Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307 console.log( @@ -362,7 +361,7 @@ module.exports = React.createClass({ // XXX: EVIL HACK to autofocus inviting on empty rooms. // We use the setTimeout to avoid racing with focus_composer. - if (this.state.room && + if (this.props.isActive !== false && this.state.room && this.state.room.getJoinedMemberCount() == 1 && this.state.room.getLiveTimeline() && this.state.room.getLiveTimeline().getEvents() && @@ -416,7 +415,7 @@ module.exports = React.createClass({ roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd); roomView.removeEventListener('dragend', this.onDragLeaveOrEnd); } - dis.unregister(this.dispatcherRef); + this.props.roomViewStore.getDispatcher().unregister(this.dispatcherRef); if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Room", this.onRoom); MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); @@ -791,7 +790,7 @@ module.exports = React.createClass({ }, onSearchResultsResize: function() { - dis.dispatch({ action: 'timeline_resize' }, true); + this.props.roomViewStore.getDispatcher().dispatch({ action: 'timeline_resize' }, true); }, onSearchResultsFillRequest: function(backwards) { @@ -812,7 +811,7 @@ module.exports = React.createClass({ onInviteButtonClick: function() { // call AddressPickerDialog - dis.dispatch({ + this.props.roomViewStore.getDispatcher().dispatch({ action: 'view_invite', roomId: this.state.room.roomId, }); @@ -834,7 +833,7 @@ module.exports = React.createClass({ // Join this room once the user has registered and logged in const signUrl = this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : undefined; - dis.dispatch({ + this.props.roomViewStore.getDispatcher().dispatch({ action: 'do_after_sync_prepared', deferred_action: { action: 'join_room', @@ -844,7 +843,7 @@ module.exports = React.createClass({ // Don't peek whilst registering otherwise getPendingEventList complains // Do this by indicating our intention to join - dis.dispatch({ + this.props.roomViewStore.getDispatcher().dispatch({ action: 'will_join', }); @@ -855,20 +854,20 @@ module.exports = React.createClass({ if (submitted) { this.props.onRegistered(credentials); } else { - dis.dispatch({ + this.props.roomViewStore.getDispatcher().dispatch({ action: 'cancel_after_sync_prepared', }); - dis.dispatch({ + this.props.roomViewStore.getDispatcher().dispatch({ action: 'cancel_join', }); } }, onDifferentServerClicked: (ev) => { - dis.dispatch({action: 'start_registration'}); + this.props.roomViewStore.getDispatcher().dispatch({action: 'start_registration'}); close(); }, onLoginClick: (ev) => { - dis.dispatch({action: 'start_login'}); + this.props.roomViewStore.getDispatcher().dispatch({action: 'start_login'}); close(); }, }).close; @@ -878,7 +877,7 @@ module.exports = React.createClass({ Promise.resolve().then(() => { const signUrl = this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : undefined; - dis.dispatch({ + this.props.roomViewStore.getDispatcher().dispatch({ action: 'join_room', opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers }, }); @@ -934,10 +933,10 @@ module.exports = React.createClass({ }, uploadFile: async function(file) { - dis.dispatch({action: 'focus_composer'}); + this.props.roomViewStore.getDispatcher().dispatch({action: 'focus_composer'}); if (MatrixClientPeg.get().isGuest()) { - dis.dispatch({action: 'require_registration'}); + this.props.roomViewStore.getDispatcher().dispatch({action: 'require_registration'}); return; } @@ -961,14 +960,14 @@ module.exports = React.createClass({ } // Send message_sent callback, for things like _checkIfAlone because after all a file is still a message. - dis.dispatch({ + this.props.roomViewStore.getDispatcher().dispatch({ action: 'message_sent', }); }, injectSticker: function(url, info, text) { if (MatrixClientPeg.get().isGuest()) { - dis.dispatch({action: 'require_registration'}); + this.props.roomViewStore.getDispatcher().dispatch({action: 'require_registration'}); return; } @@ -1169,7 +1168,7 @@ module.exports = React.createClass({ }, onSettingsClick: function() { - dis.dispatch({ action: 'open_room_settings' }); + this.props.roomViewStore.getDispatcher().dispatch({ action: 'open_room_settings' }); }, onSettingsSaveClick: function() { @@ -1202,31 +1201,31 @@ module.exports = React.createClass({ }); // still editing room settings } else { - dis.dispatch({ action: 'close_settings' }); + this.props.roomViewStore.getDispatcher().dispatch({ action: 'close_settings' }); } }).finally(() => { this.setState({ uploadingRoomSettings: false, }); - dis.dispatch({ action: 'close_settings' }); + this.props.roomViewStore.getDispatcher().dispatch({ action: 'close_settings' }); }).done(); }, onCancelClick: function() { console.log("updateTint from onCancelClick"); this.updateTint(); - dis.dispatch({ action: 'close_settings' }); + this.props.roomViewStore.getDispatcher().dispatch({ action: 'close_settings' }); if (this.state.forwardingEvent) { - dis.dispatch({ + this.props.roomViewStore.getDispatcher().dispatch({ action: 'forward_event', event: null, }); } - dis.dispatch({action: 'focus_composer'}); + this.props.roomViewStore.getDispatcher().dispatch({action: 'focus_composer'}); }, onLeaveClick: function() { - dis.dispatch({ + this.props.roomViewStore.getDispatcher().dispatch({ action: 'leave_room', room_id: this.state.room.roomId, }); @@ -1234,7 +1233,7 @@ module.exports = React.createClass({ onForgetClick: function() { MatrixClientPeg.get().forget(this.state.room.roomId).done(function() { - dis.dispatch({ action: 'view_next_room' }); + this.props.roomViewStore.getDispatcher().dispatch({ action: 'view_next_room' }); }, function(err) { const errCode = err.errcode || _t("unknown error code"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -1251,7 +1250,7 @@ module.exports = React.createClass({ rejecting: true, }); MatrixClientPeg.get().leave(this.state.roomId).done(function() { - dis.dispatch({ action: 'view_next_room' }); + this.props.roomViewStore.getDispatcher().dispatch({ action: 'view_next_room' }); self.setState({ rejecting: false, }); @@ -1277,7 +1276,7 @@ module.exports = React.createClass({ // using /leave rather than /join. In the short term though, we // just ignore them. // https://github.com/vector-im/vector-web/issues/1134 - dis.dispatch({ + this.props.roomViewStore.getDispatcher().dispatch({ action: 'view_room_directory', }); }, @@ -1296,7 +1295,7 @@ module.exports = React.createClass({ // jump down to the bottom of this room, where new events are arriving jumpToLiveTimeline: function() { this.refs.messagePanel.jumpToLiveTimeline(); - dis.dispatch({action: 'focus_composer'}); + this.props.roomViewStore.getDispatcher().dispatch({action: 'focus_composer'}); }, // jump up to wherever our read marker is @@ -1386,7 +1385,7 @@ module.exports = React.createClass({ }, onFullscreenClick: function() { - dis.dispatch({ + this.props.roomViewStore.getDispatcher().dispatch({ action: 'video_fullscreen', fullscreen: true, }, true); diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 5ac788fb1d..acbaea9ddc 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -130,7 +130,7 @@ export default class MessageComposer extends React.Component { onUploadClick(ev) { if (MatrixClientPeg.get().isGuest()) { - dis.dispatch({action: 'require_registration'}); + this.props.roomViewStore.getDispatcher().dispatch({action: 'require_registration'}); return; } @@ -192,7 +192,7 @@ export default class MessageComposer extends React.Component { if (!call) { return; } - dis.dispatch({ + this.props.roomViewStore.getDispatcher().dispatch({ action: 'hangup', // hangup the call for this room, which may not be the room in props // (e.g. conferences which will hangup the 1:1 room instead) @@ -201,7 +201,7 @@ export default class MessageComposer extends React.Component { } onCallClick(ev) { - dis.dispatch({ + this.props.roomViewStore.getDispatcher().dispatch({ action: 'place_call', type: ev.shiftKey ? "screensharing" : "video", room_id: this.props.room.roomId, @@ -209,7 +209,7 @@ export default class MessageComposer extends React.Component { } onVoiceCallClick(ev) { - dis.dispatch({ + this.props.roomViewStore.getDispatcher().dispatch({ action: 'place_call', type: "voice", room_id: this.props.room.roomId, @@ -245,7 +245,7 @@ export default class MessageComposer extends React.Component { ev.preventDefault(); const replacementRoomId = this.state.tombstone.getContent()['replacement_room']; - dis.dispatch({ + this.props.roomViewStore.getDispatcher().dispatch({ action: 'view_room', highlighted: true, room_id: replacementRoomId, diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index da41dba212..c54c0b1ab6 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -41,8 +41,6 @@ import sdk from '../../../index'; import { _t, _td } from '../../../languageHandler'; import Analytics from '../../../Analytics'; -import dis from '../../../dispatcher'; - import * as RichText from '../../../RichText'; import * as HtmlUtils from '../../../HtmlUtils'; import Autocomplete from './Autocomplete'; @@ -120,7 +118,7 @@ function onSendMessageFailed(err, room) { // XXX: temporary logging to try to diagnose // https://github.com/vector-im/riot-web/issues/3148 console.log('MessageComposer got send failure: ' + err.name + '('+err+')'); - dis.dispatch({ + this.props.roomViewStore.getDispatcher().dispatch({ action: 'message_send_failed', }); } @@ -344,12 +342,12 @@ export default class MessageComposerInput extends React.Component { } componentWillMount() { - this.dispatcherRef = dis.register(this.onAction); + this.dispatcherRef = this.props.roomViewStore.getDispatcher().register(this.onAction); this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_'); } componentWillUnmount() { - dis.unregister(this.dispatcherRef); + this.props.roomViewStore.getDispatcher().unregister(this.dispatcherRef); } _collectEditor = (e) => { @@ -1208,14 +1206,14 @@ export default class MessageComposerInput extends React.Component { // Clear reply_to_event as we put the message into the queue // if the send fails, retry will handle resending. - dis.dispatch({ + this.props.roomViewStore.getDispatcher().dispatch({ action: 'reply_to_event', event: null, }); } this.client.sendMessage(this.props.room.roomId, content).then((res) => { - dis.dispatch({ + this.props.roomViewStore.getDispatcher().dispatch({ action: 'message_sent', }); }).catch((e) => { From 74becf71d8c889bc5710076f02f5ab06166f521b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Nov 2018 16:00:11 +0000 Subject: [PATCH 20/93] add right panel back to grid view --- res/css/structures/_GroupGridView.scss | 25 ++++++++---- src/components/structures/GroupGridView.js | 45 +++++++++++++--------- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/res/css/structures/_GroupGridView.scss b/res/css/structures/_GroupGridView.scss index 9e5c5c3379..8795f958eb 100644 --- a/res/css/structures/_GroupGridView.scss +++ b/res/css/structures/_GroupGridView.scss @@ -14,13 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_GroupGridView { +.mx_GroupGridView_rooms { display: grid; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr 1fr; - background-color: red; + flex: 1 1 0; } +.mx_GroupGridView { + display: flex; + flex-direction: column; +} + +.mx_GroupGridView > .mx_MainSplit { + flex: 1 1 0; + display: flex; +} .mx_GroupGridView_emptyTile::before { display: block; @@ -67,32 +76,32 @@ limitations under the License. height: 100%; } -.mx_GroupGridView > *:nth-child(1) { +.mx_GroupGridView_rooms > *:nth-child(1) { grid-column: 1; grid-row: 1; } -.mx_GroupGridView > *:nth-child(2) { +.mx_GroupGridView_rooms > *:nth-child(2) { grid-column: 2; grid-row: 1; } -.mx_GroupGridView > *:nth-child(3) { +.mx_GroupGridView_rooms > *:nth-child(3) { grid-column: 3; grid-row: 1; } -.mx_GroupGridView > *:nth-child(4) { +.mx_GroupGridView_rooms > *:nth-child(4) { grid-column: 1; grid-row: 2; } -.mx_GroupGridView > *:nth-child(5) { +.mx_GroupGridView_rooms > *:nth-child(5) { grid-column: 2; grid-row: 2; } -.mx_GroupGridView > *:nth-child(6) { +.mx_GroupGridView_rooms > *:nth-child(6) { grid-column: 3; grid-row: 2; } diff --git a/src/components/structures/GroupGridView.js b/src/components/structures/GroupGridView.js index 0181966d49..d6268ff14c 100644 --- a/src/components/structures/GroupGridView.js +++ b/src/components/structures/GroupGridView.js @@ -21,6 +21,8 @@ import OpenRoomsStore from '../../stores/OpenRoomsStore'; import dis from '../../dispatcher'; import RoomView from './RoomView'; import classNames from 'classnames'; +import MainSplit from './MainSplit'; +import RightPanel from './RightPanel'; export default class RoomGridView extends React.Component { @@ -90,25 +92,32 @@ export default class RoomGridView extends React.Component { const emptyTiles = Array.from({length: emptyCount}, () => null); roomStores = roomStores.concat(emptyTiles); } + const activeRoomId = this.state.activeRoomStore && this.state.activeRoomStore.getRoomId(); + const rightPanel = activeRoomId ? : undefined; + return (
- { roomStores.map((roomStore, i) => { - if (roomStore) { - const isActive = roomStore === this.state.activeRoomStore; - const tileClasses = classNames({ - "mx_GroupGridView_tile": true, - "mx_GroupGridView_activeTile": isActive, - }); - return (
{this._setActive(i)}} key={roomStore.getRoomId()} className={tileClasses}> - -
); - } else { - return (
); - } - }) } + +
+ { roomStores.map((roomStore, i) => { + if (roomStore) { + const isActive = roomStore === this.state.activeRoomStore; + const tileClasses = classNames({ + "mx_GroupGridView_tile": true, + "mx_GroupGridView_activeTile": isActive, + }); + return (
{this._setActive(i)}} key={roomStore.getRoomId()} className={tileClasses}> + +
); + } else { + return (
); + } + }) } +
+
); } From ec070ea782af50fcd3bf537bd713484a4056097f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Nov 2018 16:06:05 +0000 Subject: [PATCH 21/93] use % instead of fr units for grid, make size independant of content --- res/css/structures/_GroupGridView.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/structures/_GroupGridView.scss b/res/css/structures/_GroupGridView.scss index 8795f958eb..14c46d1370 100644 --- a/res/css/structures/_GroupGridView.scss +++ b/res/css/structures/_GroupGridView.scss @@ -16,8 +16,8 @@ limitations under the License. .mx_GroupGridView_rooms { display: grid; - grid-template-columns: 1fr 1fr 1fr; - grid-template-rows: 1fr 1fr; + grid-template-columns: repeat(3, calc(100% / 3)); + grid-template-rows: repeat(2, calc(100% / 2)); flex: 1 1 0; } From f593bff3c30113c6eb94e3309716cf62e5b2aabd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Nov 2018 17:03:23 +0000 Subject: [PATCH 22/93] show right panel tabs inside panel instead of room header in grid mode --- res/css/structures/_GroupGridView.scss | 26 +++++++++++++++++++++- res/css/views/rooms/_MemberList.scss | 4 ++++ src/components/structures/GroupGridView.js | 13 +++++++++-- src/components/structures/RoomView.js | 7 +++++- src/components/views/rooms/RoomHeader.js | 2 +- 5 files changed, 47 insertions(+), 5 deletions(-) diff --git a/res/css/structures/_GroupGridView.scss b/res/css/structures/_GroupGridView.scss index 14c46d1370..0464026ed4 100644 --- a/res/css/structures/_GroupGridView.scss +++ b/res/css/structures/_GroupGridView.scss @@ -14,6 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_GroupGridView { + display: flex; + flex-direction: column; +} + .mx_GroupGridView_rooms { display: grid; grid-template-columns: repeat(3, calc(100% / 3)); @@ -21,11 +26,30 @@ limitations under the License. flex: 1 1 0; } -.mx_GroupGridView { + +.mx_GroupGridView_rightPanel { display: flex; flex-direction: column; + + .mx_GroupGridView_tabs { + flex: 0 0 52px; + border-bottom: 1px solid $primary-hairline-color; + display: flex; + align-items: center; + + > div { + justify-content: flex-end; + width: 100%; + margin-right: 10px; + } + } + + .mx_RightPanel { + flex: 1 0 auto !important; + } } + .mx_GroupGridView > .mx_MainSplit { flex: 1 1 0; display: flex; diff --git a/res/css/views/rooms/_MemberList.scss b/res/css/views/rooms/_MemberList.scss index 2695ebcf31..8e59eb85d5 100644 --- a/res/css/views/rooms/_MemberList.scss +++ b/res/css/views/rooms/_MemberList.scss @@ -53,6 +53,10 @@ limitations under the License. .mx_MemberList_query, .mx_GroupMemberList_query, .mx_GroupRoomList_query { + flex: 0 0 auto; +} + +.mx_MemberList .gm-scrollbar-container { flex: 1 1 0; } diff --git a/src/components/structures/GroupGridView.js b/src/components/structures/GroupGridView.js index d6268ff14c..abfd343309 100644 --- a/src/components/structures/GroupGridView.js +++ b/src/components/structures/GroupGridView.js @@ -23,6 +23,7 @@ import RoomView from './RoomView'; import classNames from 'classnames'; import MainSplit from './MainSplit'; import RightPanel from './RightPanel'; +import RoomHeaderButtons from '../views/right_panel/RoomHeaderButtons'; export default class RoomGridView extends React.Component { @@ -93,7 +94,15 @@ export default class RoomGridView extends React.Component { roomStores = roomStores.concat(emptyTiles); } const activeRoomId = this.state.activeRoomStore && this.state.activeRoomStore.getRoomId(); - const rightPanel = activeRoomId ? : undefined; + let rightPanel; + if (activeRoomId) { + rightPanel = ( +
+
+ +
+ ); + } return (
@@ -107,7 +116,7 @@ export default class RoomGridView extends React.Component { }); return (
{this._setActive(i)}} key={roomStore.getRoomId()} className={tileClasses}> diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index f6b9381080..af389c804a 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1509,6 +1509,7 @@ module.exports = React.createClass({
@@ -1555,6 +1556,7 @@ module.exports = React.createClass({
@@ -1813,11 +1815,14 @@ module.exports = React.createClass({ }, ); - const rightPanel = this.state.room ? : undefined; + const rightPanel = this.state.room && !this.props.isGrid ? + : + undefined; return (
+ { !this.props.isGrid ? : undefined }
); From c8243357eac5509d6e920df0928450c939165d98 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Nov 2018 18:32:57 +0000 Subject: [PATCH 23/93] disable editor history/persistence when in grid to avoid pesky bug --- src/components/structures/RoomView.js | 1 + src/components/views/rooms/MessageComposer.js | 1 + .../views/rooms/MessageComposerInput.js | 21 ++++++++++++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index af389c804a..681b1221e1 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1690,6 +1690,7 @@ module.exports = React.createClass({ this.messageComposerInput = c} key="controls_input" + isGrid={this.props.isGrid} onResize={this.props.onResize} room={this.props.room} placeholder={placeholderText} diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index c54c0b1ab6..0740667242 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -132,6 +132,18 @@ function rangeEquals(a: Range, b: Range): boolean { && a.isBackward === b.isBackward); } +class NoopHistoryManager { + getItem() {} + save() {} + + get currentIndex() { return 0; } + set currentIndex(_) {} + + get history() { return []; } + set history(_) {} +} + + /* * The textInput part of the MessageComposer */ @@ -343,7 +355,14 @@ export default class MessageComposerInput extends React.Component { componentWillMount() { this.dispatcherRef = this.props.roomViewStore.getDispatcher().register(this.onAction); - this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_'); + if (this.props.isGrid) { + + + + this.historyManager = new NoopHistoryManager(); + } else { + this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_'); + } } componentWillUnmount() { From b0c84591d7300df0c46f948cc1b461c7b930b9a4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Nov 2018 18:35:14 +0000 Subject: [PATCH 24/93] show focus glow below dialogs (at z-index 4000) --- res/css/structures/_GroupGridView.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/structures/_GroupGridView.scss b/res/css/structures/_GroupGridView.scss index 0464026ed4..9e51af25f9 100644 --- a/res/css/structures/_GroupGridView.scss +++ b/res/css/structures/_GroupGridView.scss @@ -80,7 +80,7 @@ limitations under the License. bottom: 0; content: ""; pointer-events: none; - z-index: 10000; + z-index: 3500; } .mx_GroupGridView_activeTile:before { From 0ffd77762acd0283faa5bee217343328db9d7261 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Nov 2018 18:54:27 +0000 Subject: [PATCH 25/93] make menu option look somewhat better --- res/img/icons-gridview.svg | 103 ++++++++++++++++++ .../views/context_menus/TagTileContextMenu.js | 6 + src/i18n/strings/en_EN.json | 3 +- 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 res/img/icons-gridview.svg diff --git a/res/img/icons-gridview.svg b/res/img/icons-gridview.svg new file mode 100644 index 0000000000..862ca63765 --- /dev/null +++ b/res/img/icons-gridview.svg @@ -0,0 +1,103 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/src/components/views/context_menus/TagTileContextMenu.js b/src/components/views/context_menus/TagTileContextMenu.js index b8930efefe..5cd5aea27a 100644 --- a/src/components/views/context_menus/TagTileContextMenu.js +++ b/src/components/views/context_menus/TagTileContextMenu.js @@ -75,6 +75,12 @@ export default class TagTileContextMenu extends React.Component { { _t('View Community') }
+ { _t('View as grid') }

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c137253a43..f592ad6441 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1348,5 +1348,6 @@ "Import": "Import", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", - "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" + "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room", + "View as grid": "View as grid" } From 368ef9e8e89b0e75c26e4912049002d4558a495f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Nov 2018 18:57:07 +0000 Subject: [PATCH 26/93] hack so we don't revert to single room view when viewing grid --- src/stores/OpenRoomsStore.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/stores/OpenRoomsStore.js b/src/stores/OpenRoomsStore.js index f67108d35a..764f54d7c2 100644 --- a/src/stores/OpenRoomsStore.js +++ b/src/stores/OpenRoomsStore.js @@ -102,10 +102,15 @@ class OpenRoomsStore extends Store { const dispatcher = new MatrixDispatcher(); // forward all actions coming from the room dispatcher // to the global one - const dispatcherRef = dispatcher.register((action) => { - action.grid_src_room_id = room_id; - action.grid_src_room_alias = room_alias; - this.getDispatcher().dispatch(action); + const dispatcherRef = dispatcher.register((payload) => { + // block a view_room action for the same room because it will switch to + // single room mode in MatrixChat + if (payload.action === 'view_room' && room_id === payload.room_id) { + return; + } + payload.grid_src_room_id = room_id; + payload.grid_src_room_alias = room_alias; + this.getDispatcher().dispatch(payload); }); const openRoom = { store: new RoomViewStore(dispatcher), From 04bb13bb7b1d711c3a51b86d46640475ff130f60 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Nov 2018 18:57:37 +0000 Subject: [PATCH 27/93] emit join error over own dispatcher, meh --- src/stores/RoomViewStore.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index af1264921a..1e0f5c6a4f 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -14,7 +14,6 @@ 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 dis from '../dispatcher'; import {Store} from 'flux/utils'; import MatrixClientPeg from '../MatrixClientPeg'; import sdk from '../index'; @@ -191,7 +190,7 @@ export class RoomViewStore extends Store { // stream yet, and that's the point at which we'd consider // the user joined to the room. }, (err) => { - dis.dispatch({ + this.getDispatcher().dispatch({ action: 'join_room_error', err: err, }); From 1810c17d24f2165d268c7c35be96a35b1b009758 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Nov 2018 18:58:12 +0000 Subject: [PATCH 28/93] remove trace, add comment, ... --- src/components/structures/MatrixChat.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 3d3aa52e57..b67dc7b352 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -837,8 +837,7 @@ export default React.createClass({ // room name and avatar from an invite email) _viewRoom: function(roomInfo) { this.focusComposer = true; - console.log("!!! MatrixChat._viewRoom", roomInfo); - console.trace(); + console.log("!!! MatrixChat._viewRoom", roomInfo); const newState = { currentRoomId: roomInfo.room_id || null, @@ -887,6 +886,9 @@ export default React.createClass({ if (roomInfo.event_id && roomInfo.highlighted) { presentedId += "/" + roomInfo.event_id; } + + + // TODO: only emit this when we're not in grid mode? this.notifyNewScreen('room/' + presentedId); newState.ready = true; this.setState(newState); From 39289e57ac46b401a95bff3e53c7addfed3b761d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 22 Nov 2018 19:06:58 +0000 Subject: [PATCH 29/93] fix correct room being highlighted in left panel --- src/ActiveRoomObserver.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/ActiveRoomObserver.js b/src/ActiveRoomObserver.js index f3850dd87c..c276cccb5d 100644 --- a/src/ActiveRoomObserver.js +++ b/src/ActiveRoomObserver.js @@ -55,24 +55,23 @@ class ActiveRoomObserver { } } - _emit(roomId) { + _emit(roomId, newActiveRoomId) { if (!this._listeners[roomId]) return; for (const l of this._listeners[roomId]) { - l.call(); + l.call(l, newActiveRoomId); } } _onOpenRoomsStoreUpdate() { - // emit for the old room ID - if (this._activeRoomId) this._emit(this._activeRoomId); - const activeRoomStore = OpenRoomsStore.getActiveRoomStore(); + const newActiveRoomId = activeRoomStore && activeRoomStore.getRoomId(); + // emit for the old room ID + if (this._activeRoomId) this._emit(this._activeRoomId, newActiveRoomId); // update our cache - this._activeRoomId = activeRoomStore && activeRoomStore.getRoomId(); - + this._activeRoomId = newActiveRoomId; // and emit for the new one - if (this._activeRoomId) this._emit(this._activeRoomId); + if (this._activeRoomId) this._emit(this._activeRoomId, this._activeRoomId); } } From 7b6c8633772c0deff41e59f1d2b582ff5efeeb27 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 14 Dec 2018 15:53:52 +0100 Subject: [PATCH 30/93] fix lint --- src/components/structures/GroupGridView.js | 19 +++++++++++-------- src/stores/OpenRoomsStore.js | 7 ++++--- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/components/structures/GroupGridView.js b/src/components/structures/GroupGridView.js index abfd343309..b80eeced0c 100644 --- a/src/components/structures/GroupGridView.js +++ b/src/components/structures/GroupGridView.js @@ -16,7 +16,6 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import OpenRoomsStore from '../../stores/OpenRoomsStore'; import dis from '../../dispatcher'; import RoomView from './RoomView'; @@ -114,13 +113,17 @@ export default class RoomGridView extends React.Component { "mx_GroupGridView_tile": true, "mx_GroupGridView_activeTile": isActive, }); - return (
{this._setActive(i)}} key={roomStore.getRoomId()} className={tileClasses}> - -
); + return (
{this._setActive(i);}} + key={roomStore.getRoomId()} + className={tileClasses} + > + +
); } else { return (
); } diff --git a/src/stores/OpenRoomsStore.js b/src/stores/OpenRoomsStore.js index 764f54d7c2..fabed60c9e 100644 --- a/src/stores/OpenRoomsStore.js +++ b/src/stores/OpenRoomsStore.js @@ -182,7 +182,7 @@ class OpenRoomsStore extends Store { auto_join: payload.auto_join, oob_data: payload.oob_data, }); - } catch(err) { + } catch (err) { this._forwardAction({ action: 'view_room_error', room_id: null, @@ -223,6 +223,7 @@ class OpenRoomsStore extends Store { } __onDispatch(payload) { + let proposedIndex; switch (payload.action) { // view_room: // - room_alias: '#somealias:matrix.org' @@ -253,10 +254,10 @@ class OpenRoomsStore extends Store { this._forwardingEvent = payload.event; break; case 'group_grid_set_active': - const proposedIndex = this._roomIndex(payload); + proposedIndex = this._roomIndex(payload); if (proposedIndex !== -1) { this._setState({ - currentIndex: proposedIndex + currentIndex: proposedIndex, }); } break; From 712241d71074f5c937911ed4eb68b60910a69871 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 24 Dec 2018 14:27:50 +0000 Subject: [PATCH 31/93] Add simple state counters to room heading --- res/css/_components.scss | 1 + res/css/views/rooms/_AuxPanel.scss | 48 +++++++++++++++++ src/components/views/rooms/AuxPanel.js | 73 ++++++++++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 res/css/views/rooms/_AuxPanel.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index d8f966603d..1f99699e73 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -91,6 +91,7 @@ @import "./views/messages/_UnknownBody.scss"; @import "./views/rooms/_AppsDrawer.scss"; @import "./views/rooms/_Autocomplete.scss"; +@import "./views/rooms/_AuxPanel.scss"; @import "./views/rooms/_EntityTile.scss"; @import "./views/rooms/_EventTile.scss"; @import "./views/rooms/_LinkPreviewWidget.scss"; diff --git a/res/css/views/rooms/_AuxPanel.scss b/res/css/views/rooms/_AuxPanel.scss new file mode 100644 index 0000000000..690a5e5b55 --- /dev/null +++ b/res/css/views/rooms/_AuxPanel.scss @@ -0,0 +1,48 @@ +/* +Copyright 2018 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. +*/ + +.m_RoomView_auxPanel_stateViews { + padding-top: 3px; +} + +.m_RoomView_auxPanel_stateViews_span a { + text-decoration: none; + color: inherit; +} + +.m_RoomView_auxPanel_stateViews_span[data-severity=warning] { + font-weight: bold; + color: orange; +} + +.m_RoomView_auxPanel_stateViews_span[data-severity=alert] { + font-weight: bold; + color: red; +} + +.m_RoomView_auxPanel_stateViews_span[data-severity=normal] { + font-weight: normal; +} + +.m_RoomView_auxPanel_stateViews_span[data-severity=notice] { + font-weight: normal; + color: $settings-grey-fg-color; +} + +.m_RoomView_auxPanel_stateViews_delim { + padding: 0 5px; + color: $settings-grey-fg-color; +} \ No newline at end of file diff --git a/src/components/views/rooms/AuxPanel.js b/src/components/views/rooms/AuxPanel.js index 64c0478d41..32abb41ce2 100644 --- a/src/components/views/rooms/AuxPanel.js +++ b/src/components/views/rooms/AuxPanel.js @@ -24,6 +24,7 @@ import ObjectUtils from '../../../ObjectUtils'; import AppsDrawer from './AppsDrawer'; import { _t } from '../../../languageHandler'; import classNames from 'classnames'; +import RateLimitedFunc from '../../../ratelimitedfunc'; module.exports = React.createClass({ @@ -60,6 +61,18 @@ module.exports = React.createClass({ hideAppsDrawer: false, }, + componentDidMount: function() { + const cli = MatrixClientPeg.get(); + cli.on("RoomState.events", this._rateLimitedUpdate); + }, + + componentWillUnmount: function() { + const cli = MatrixClientPeg.get(); + if (cli) { + cli.removeListener("RoomState.events", this._rateLimitedUpdate); + } + }, + shouldComponentUpdate: function(nextProps, nextState) { return (!ObjectUtils.shallowEqual(this.props, nextProps) || !ObjectUtils.shallowEqual(this.state, nextState)); @@ -82,6 +95,11 @@ module.exports = React.createClass({ ev.preventDefault(); }, + _rateLimitedUpdate: new RateLimitedFunc(function() { + /* eslint-disable babel/no-invalid-this */ + this.forceUpdate(); + }, 500), + render: function() { const CallView = sdk.getComponent("voip.CallView"); const TintableSvg = sdk.getComponent("elements.TintableSvg"); @@ -145,6 +163,60 @@ module.exports = React.createClass({ hide={this.props.hideAppsDrawer} />; + let stateViews = null; + if (this.props.room) { + const stateEvs = this.props.room.currentState.getStateEvents('re.jki.counter'); + + let counters = []; + + stateEvs.forEach((ev, idx) => { + const title = ev.getContent().title; + const value = ev.getContent().value; + const link = ev.getContent().link; + const severity = ev.getContent().severity || "normal"; + const stateKey = ev.getStateKey(); + + if (title && value && severity) { + let span = { title }: { value } + + if (link) { + span = ( + + { span } + + ); + } + + span = ( + + {span} + + ); + + counters.push(span); + counters.push( + ─ + ); + } + }); + + if (counters.length > 0) { + counters.pop(); // remove last deliminator + stateViews = ( +
+ { counters } +
+ ); + } + } + const classes = classNames({ "mx_RoomView_auxPanel": true, "mx_RoomView_auxPanel_fullHeight": this.props.fullHeight, @@ -156,6 +228,7 @@ module.exports = React.createClass({ return (
+ { stateViews } { appsDrawer } { fileDropTarget } { callView } From 4c204e88be2364468c0b1fa27ca0a60d1065c129 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 24 Dec 2018 15:09:10 +0000 Subject: [PATCH 32/93] Add feature flag for counters --- src/components/views/rooms/AuxPanel.js | 3 ++- src/i18n/strings/en_EN.json | 3 ++- src/settings/Settings.js | 6 ++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/AuxPanel.js b/src/components/views/rooms/AuxPanel.js index 32abb41ce2..dd11ee8ad0 100644 --- a/src/components/views/rooms/AuxPanel.js +++ b/src/components/views/rooms/AuxPanel.js @@ -25,6 +25,7 @@ import AppsDrawer from './AppsDrawer'; import { _t } from '../../../languageHandler'; import classNames from 'classnames'; import RateLimitedFunc from '../../../ratelimitedfunc'; +import SettingsStore from "../../../settings/SettingsStore"; module.exports = React.createClass({ @@ -164,7 +165,7 @@ module.exports = React.createClass({ />; let stateViews = null; - if (this.props.room) { + if (this.props.room && SettingsStore.isFeatureEnabled("feature_state_counters")) { const stateEvs = this.props.room.currentState.getStateEvents('re.jki.counter'); let counters = []; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 85cb8c9868..a6b04ec338 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1404,5 +1404,6 @@ "Go to Settings": "Go to Settings", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", - "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" + "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room", + "Render simple counters in room header": "Render simple counters in room header" } diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 14f4bdc6dd..ec7dadc341 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -102,6 +102,12 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_state_counters": { + isFeature: true, + displayName: _td("Render simple counters in room header"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "MessageComposerInput.dontSuggestEmoji": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Disable Emoji suggestions while typing'), From 8a6e7382aec4d7d976c18b5c0fe6ff4c6093c847 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 24 Dec 2018 15:11:05 +0000 Subject: [PATCH 33/93] Missing newline --- res/css/views/rooms/_AuxPanel.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_AuxPanel.scss b/res/css/views/rooms/_AuxPanel.scss index 690a5e5b55..808ac2c6c7 100644 --- a/res/css/views/rooms/_AuxPanel.scss +++ b/res/css/views/rooms/_AuxPanel.scss @@ -45,4 +45,4 @@ limitations under the License. .m_RoomView_auxPanel_stateViews_delim { padding: 0 5px; color: $settings-grey-fg-color; -} \ No newline at end of file +} From b63bd5ea5453c08d05dd6002c5c6c6d704647b00 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 7 Jan 2019 14:59:00 +0100 Subject: [PATCH 34/93] allow right panel to be hidden (although container is still visible) --- src/components/structures/GroupGridView.js | 4 ++-- src/components/structures/LoggedInView.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/structures/GroupGridView.js b/src/components/structures/GroupGridView.js index b80eeced0c..ece4e5fa23 100644 --- a/src/components/structures/GroupGridView.js +++ b/src/components/structures/GroupGridView.js @@ -97,8 +97,8 @@ export default class RoomGridView extends React.Component { if (activeRoomId) { rightPanel = (
-
- +
+ { !this.props.collapsedRhs ? : undefined }
); } diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 74274a5be2..67d7d41701 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -452,7 +452,7 @@ const LoggedInView = React.createClass({ />; break; case PageTypes.GroupGridView: - page_element = ; + page_element = ; break; case PageTypes.UserSettings: page_element = Date: Mon, 7 Jan 2019 15:20:39 +0100 Subject: [PATCH 35/93] clear width of right panel container when collapsed in grid view --- src/components/structures/GroupGridView.js | 2 +- src/components/structures/MainSplit.js | 15 +++++++++------ src/resizer/resizer.js | 10 +++++++++- src/resizer/sizer.js | 8 ++++++++ 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/components/structures/GroupGridView.js b/src/components/structures/GroupGridView.js index ece4e5fa23..c8d5f59323 100644 --- a/src/components/structures/GroupGridView.js +++ b/src/components/structures/GroupGridView.js @@ -104,7 +104,7 @@ export default class RoomGridView extends React.Component { } return (
- +
{ roomStores.map((roomStore, i) => { if (roomStore) { diff --git a/src/components/structures/MainSplit.js b/src/components/structures/MainSplit.js index 0427130eea..aa27930ce0 100644 --- a/src/components/structures/MainSplit.js +++ b/src/components/structures/MainSplit.js @@ -71,14 +71,17 @@ export default class MainSplit extends React.Component { } componentDidUpdate(prevProps) { - const wasExpanded = !this.props.collapsedRhs && prevProps.collapsedRhs; - const wasCollapsed = this.props.collapsedRhs && !prevProps.collapsedRhs; - const wasPanelSet = this.props.panel && !prevProps.panel; - const wasPanelCleared = !this.props.panel && prevProps.panel; + const shouldAllowResizing = + !this.props.disableSizing && + !this.props.collapsedRhs && + this.props.panel; - if (wasExpanded || wasPanelSet) { + if (shouldAllowResizing && !this.resizer) { this._createResizer(); - } else if (wasCollapsed || wasPanelCleared) { + } else if (!shouldAllowResizing && this.resizer) { + if (this.props.disableSizing) { + this.resizer.clearItemSizes(); + } this.resizer.detach(); this.resizer = null; } diff --git a/src/resizer/resizer.js b/src/resizer/resizer.js index 0e113b3664..ff2120b341 100644 --- a/src/resizer/resizer.js +++ b/src/resizer/resizer.js @@ -43,6 +43,14 @@ export class Resizer { this._onMouseDown = this._onMouseDown.bind(this); } + clearItemSizes() { + const handles = this._getResizeHandles(); + handles.forEach((handle) => { + const {sizer, item} = this._createSizerAndDistributor(handle); + sizer.clearItemSize(item); + }); + } + setClassNames(classNames) { this.classNames = classNames; } @@ -134,7 +142,7 @@ export class Resizer { const distributor = new this.distributorCtor( sizer, item, id, this.distributorCfg, items, this.container); - return {sizer, distributor}; + return {sizer, distributor, item}; } _getResizableItems() { diff --git a/src/resizer/sizer.js b/src/resizer/sizer.js index 303214854b..0e2236814e 100644 --- a/src/resizer/sizer.js +++ b/src/resizer/sizer.js @@ -82,6 +82,14 @@ class Sizer { } } + clearItemSize(item, size) { + if (this.vertical) { + item.style.height = null; + } else { + item.style.width = null; + } + } + /** @param {MouseEvent} event the mouse event @return {number} the distance between the cursor and the edge of the container, From 8fa7ec0fac7df925de97cc400691999b7ecc1e65 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 7 Jan 2019 16:03:24 +0100 Subject: [PATCH 36/93] fix lint --- src/components/structures/GroupGridView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/GroupGridView.js b/src/components/structures/GroupGridView.js index c8d5f59323..4c488941d0 100644 --- a/src/components/structures/GroupGridView.js +++ b/src/components/structures/GroupGridView.js @@ -104,7 +104,7 @@ export default class RoomGridView extends React.Component { } return (
- +
{ roomStores.map((roomStore, i) => { if (roomStore) { From c2f6fc381cf63a63bc4c9fab093ecccacbb5bbd9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 7 Jan 2019 16:13:35 +0100 Subject: [PATCH 37/93] add feature flag for grid view --- .../views/context_menus/TagTileContextMenu.js | 23 +++++++++++-------- src/i18n/strings/en_EN.json | 3 ++- src/settings/Settings.js | 6 +++++ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/components/views/context_menus/TagTileContextMenu.js b/src/components/views/context_menus/TagTileContextMenu.js index 5cd5aea27a..ad93783485 100644 --- a/src/components/views/context_menus/TagTileContextMenu.js +++ b/src/components/views/context_menus/TagTileContextMenu.js @@ -21,6 +21,7 @@ import dis from '../../../dispatcher'; import TagOrderActions from '../../../actions/TagOrderActions'; import MatrixClientPeg from '../../../MatrixClientPeg'; import sdk from '../../../index'; +import SettingsStore from "../../../settings/SettingsStore"; export default class TagTileContextMenu extends React.Component { static propTypes = { @@ -64,6 +65,18 @@ export default class TagTileContextMenu extends React.Component { render() { const TintableSvg = sdk.getComponent("elements.TintableSvg"); + let gridViewOption; + if (SettingsStore.isFeatureEnabled("feature_gridview")) { + gridViewOption = (
+ + { _t('View as grid') } +
); + } return
{ _t('View Community') }
-
- - { _t('View as grid') } -
+ { gridViewOption }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8822bd6388..4fab9ec438 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1405,5 +1405,6 @@ "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room", - "View as grid": "View as grid" + "View as grid": "View as grid", + "Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu": "Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu" } diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 14f4bdc6dd..8edec434bf 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -102,6 +102,12 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_gridview": { + isFeature: true, + displayName: _td("Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "MessageComposerInput.dontSuggestEmoji": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Disable Emoji suggestions while typing'), From 9a5f17d0b17638cb272a251502409365fde9fdcd Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 7 Jan 2019 15:21:00 +0000 Subject: [PATCH 38/93] Change CSS to match experimental --- res/css/views/rooms/_AuxPanel.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_AuxPanel.scss b/res/css/views/rooms/_AuxPanel.scss index 808ac2c6c7..34ef5e01d4 100644 --- a/res/css/views/rooms/_AuxPanel.scss +++ b/res/css/views/rooms/_AuxPanel.scss @@ -15,7 +15,9 @@ limitations under the License. */ .m_RoomView_auxPanel_stateViews { - padding-top: 3px; + padding: 5px; + padding-left: 19px; + border-bottom: 1px solid #e5e5e5; } .m_RoomView_auxPanel_stateViews_span a { From aedc220b62a2414251c792fb111b9d421d7e17f6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 7 Jan 2019 16:33:23 +0100 Subject: [PATCH 39/93] fix (some) lint warnings --- src/components/structures/GroupGridView.js | 2 -- src/stores/OpenRoomsStore.js | 18 +++++++++--------- src/utils/Timer.js | 1 - 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/components/structures/GroupGridView.js b/src/components/structures/GroupGridView.js index 4c488941d0..2eb3c357df 100644 --- a/src/components/structures/GroupGridView.js +++ b/src/components/structures/GroupGridView.js @@ -25,7 +25,6 @@ import RightPanel from './RightPanel'; import RoomHeaderButtons from '../views/right_panel/RoomHeaderButtons'; export default class RoomGridView extends React.Component { - constructor(props) { super(props); this.state = { @@ -132,5 +131,4 @@ export default class RoomGridView extends React.Component {
); } - } diff --git a/src/stores/OpenRoomsStore.js b/src/stores/OpenRoomsStore.js index fabed60c9e..21f02fe28d 100644 --- a/src/stores/OpenRoomsStore.js +++ b/src/stores/OpenRoomsStore.js @@ -98,18 +98,18 @@ class OpenRoomsStore extends Store { }); } - _createOpenRoom(room_id, room_alias) { + _createOpenRoom(roomId, roomAlias) { const dispatcher = new MatrixDispatcher(); // forward all actions coming from the room dispatcher // to the global one const dispatcherRef = dispatcher.register((payload) => { // block a view_room action for the same room because it will switch to // single room mode in MatrixChat - if (payload.action === 'view_room' && room_id === payload.room_id) { + if (payload.action === 'view_room' && roomId === payload.room_id) { return; } - payload.grid_src_room_id = room_id; - payload.grid_src_room_alias = room_alias; + payload.grid_src_room_id = roomId; + payload.grid_src_room_alias = roomAlias; this.getDispatcher().dispatch(payload); }); const openRoom = { @@ -120,8 +120,8 @@ class OpenRoomsStore extends Store { dispatcher.dispatch({ action: 'view_room', - room_id: room_id, - room_alias: room_alias, + room_id: roomId, + room_alias: roomAlias, }, true); return openRoom; @@ -134,16 +134,16 @@ class OpenRoomsStore extends Store { }); } - _setGroupOpenRooms(group_id) { + _setGroupOpenRooms(groupId) { this._cleanupOpenRooms(); // TODO: register to GroupStore updates - const rooms = GroupStore.getGroupRooms(group_id); + const rooms = GroupStore.getGroupRooms(groupId); const openRooms = rooms.map((room) => { return this._createOpenRoom(room.roomId); }); this._setState({ rooms: openRooms, - group_id: group_id, + group_id: groupId, currentIndex: 0, }); } diff --git a/src/utils/Timer.js b/src/utils/Timer.js index aeac0887c9..4953cf54f7 100644 --- a/src/utils/Timer.js +++ b/src/utils/Timer.js @@ -26,7 +26,6 @@ Once a timer is finished or aborted, it can't be started again a new one through `clone()` or `cloneIfRun()`. */ export default class Timer { - constructor(timeout) { this._timeout = timeout; this._onTimeout = this._onTimeout.bind(this); From c6952ba5b603a3d5285355f3f69b3e28ed9e717b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 7 Jan 2019 16:56:35 +0100 Subject: [PATCH 40/93] fix some more lint warnings, as limit is 16 now --- src/components/views/right_panel/HeaderButtons.js | 1 - src/utils/Timer.js | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/right_panel/HeaderButtons.js b/src/components/views/right_panel/HeaderButtons.js index f0479eb8be..3f5f58121d 100644 --- a/src/components/views/right_panel/HeaderButtons.js +++ b/src/components/views/right_panel/HeaderButtons.js @@ -78,7 +78,6 @@ export default class HeaderButtons extends React.Component { // till show_right_panel, just without the fromHeader flag // as that would hide the right panel again dis.dispatch(Object.assign({}, payload, {fromHeader: false})); - } this.setState({ phase: payload.phase, diff --git a/src/utils/Timer.js b/src/utils/Timer.js index 4953cf54f7..ca06237fbf 100644 --- a/src/utils/Timer.js +++ b/src/utils/Timer.js @@ -69,6 +69,7 @@ export default class Timer { /** * if not started before, starts the timer. + * @returns {Timer} the same timer */ start() { if (!this.isRunning()) { @@ -80,6 +81,7 @@ export default class Timer { /** * (re)start the timer. If it's running, reset the timeout. If not, start it. + * @returns {Timer} the same timer */ restart() { if (this.isRunning()) { @@ -97,6 +99,7 @@ export default class Timer { /** * if the timer is running, abort it, * and reject the promise for this timer. + * @returns {Timer} the same timer */ abort() { if (this.isRunning()) { From c9272c48e0834f953743ccb01911743c0bfa663a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 8 Jan 2019 12:05:06 +0100 Subject: [PATCH 41/93] undo unneeded changes --- src/components/structures/MainSplit.js | 4 ---- src/resizer/resizer.js | 10 +--------- src/resizer/sizer.js | 8 -------- 3 files changed, 1 insertion(+), 21 deletions(-) diff --git a/src/components/structures/MainSplit.js b/src/components/structures/MainSplit.js index aa27930ce0..e1bbde1d97 100644 --- a/src/components/structures/MainSplit.js +++ b/src/components/structures/MainSplit.js @@ -72,16 +72,12 @@ export default class MainSplit extends React.Component { componentDidUpdate(prevProps) { const shouldAllowResizing = - !this.props.disableSizing && !this.props.collapsedRhs && this.props.panel; if (shouldAllowResizing && !this.resizer) { this._createResizer(); } else if (!shouldAllowResizing && this.resizer) { - if (this.props.disableSizing) { - this.resizer.clearItemSizes(); - } this.resizer.detach(); this.resizer = null; } diff --git a/src/resizer/resizer.js b/src/resizer/resizer.js index ff2120b341..0e113b3664 100644 --- a/src/resizer/resizer.js +++ b/src/resizer/resizer.js @@ -43,14 +43,6 @@ export class Resizer { this._onMouseDown = this._onMouseDown.bind(this); } - clearItemSizes() { - const handles = this._getResizeHandles(); - handles.forEach((handle) => { - const {sizer, item} = this._createSizerAndDistributor(handle); - sizer.clearItemSize(item); - }); - } - setClassNames(classNames) { this.classNames = classNames; } @@ -142,7 +134,7 @@ export class Resizer { const distributor = new this.distributorCtor( sizer, item, id, this.distributorCfg, items, this.container); - return {sizer, distributor, item}; + return {sizer, distributor}; } _getResizableItems() { diff --git a/src/resizer/sizer.js b/src/resizer/sizer.js index 0e2236814e..303214854b 100644 --- a/src/resizer/sizer.js +++ b/src/resizer/sizer.js @@ -82,14 +82,6 @@ class Sizer { } } - clearItemSize(item, size) { - if (this.vertical) { - item.style.height = null; - } else { - item.style.width = null; - } - } - /** @param {MouseEvent} event the mouse event @return {number} the distance between the cursor and the edge of the container, From 7227049c2a98db1b1c7fee33a342e7be7a86055b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 8 Jan 2019 12:05:39 +0100 Subject: [PATCH 42/93] add right panel toggle button to room header when in grid --- res/img/feather-icons/toggle-right-panel.svg | 17 +++++++++++++++++ src/components/structures/GroupGridView.js | 7 ++++--- src/components/views/rooms/RoomHeader.js | 17 +++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 res/img/feather-icons/toggle-right-panel.svg diff --git a/res/img/feather-icons/toggle-right-panel.svg b/res/img/feather-icons/toggle-right-panel.svg new file mode 100644 index 0000000000..4cadf89564 --- /dev/null +++ b/res/img/feather-icons/toggle-right-panel.svg @@ -0,0 +1,17 @@ + + + + Group 2 + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/structures/GroupGridView.js b/src/components/structures/GroupGridView.js index 2eb3c357df..b5e511db41 100644 --- a/src/components/structures/GroupGridView.js +++ b/src/components/structures/GroupGridView.js @@ -96,14 +96,14 @@ export default class RoomGridView extends React.Component { if (activeRoomId) { rightPanel = (
-
- { !this.props.collapsedRhs ? : undefined } +
+
); } return (
- +
{ roomStores.map((roomStore, i) => { if (roomStore) { @@ -118,6 +118,7 @@ export default class RoomGridView extends React.Component { className={tileClasses} > ; } + let toggleRightPanelButton; + if (this.props.isGrid) { + toggleRightPanelButton = + + ; + } + return (
@@ -420,6 +436,7 @@ module.exports = React.createClass({ { cancelButton } { rightRow } { !this.props.isGrid ? : undefined } + { toggleRightPanelButton }
); From 56726ba8e592bc9496f5f6c95ce3211ffd908478 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 8 Jan 2019 12:10:42 +0100 Subject: [PATCH 43/93] fix lint --- src/UserActivity.js | 1 + src/components/views/rooms/RoomHeader.js | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/UserActivity.js b/src/UserActivity.js index 4e3667274c..145b23e36e 100644 --- a/src/UserActivity.js +++ b/src/UserActivity.js @@ -44,6 +44,7 @@ class UserActivity { * Can be called multiple times with the same already running timer, which is a NO-OP. * Can be called before the user becomes active, in which case it is only started * later on when the user does become active. + * @param {Timer} timer the timer to use */ timeWhileActive(timer) { // important this happens first diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 12258154ef..91ca73dd59 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -420,7 +420,11 @@ module.exports = React.createClass({ let toggleRightPanelButton; if (this.props.isGrid) { - toggleRightPanelButton = + toggleRightPanelButton = + ; } From 07c2010cce92fcc7c3726b1b150bf8096560f116 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 8 Jan 2019 11:14:31 +0000 Subject: [PATCH 44/93] Use state rather than forceUpdate --- src/components/views/rooms/AuxPanel.js | 54 ++++++++++++++++++++------ 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/src/components/views/rooms/AuxPanel.js b/src/components/views/rooms/AuxPanel.js index dd11ee8ad0..48317411c9 100644 --- a/src/components/views/rooms/AuxPanel.js +++ b/src/components/views/rooms/AuxPanel.js @@ -62,6 +62,10 @@ module.exports = React.createClass({ hideAppsDrawer: false, }, + getInitialState: function() { + return { counters: this._computeCounters() }; + }, + componentDidMount: function() { const cli = MatrixClientPeg.get(); cli.on("RoomState.events", this._rateLimitedUpdate); @@ -97,10 +101,40 @@ module.exports = React.createClass({ }, _rateLimitedUpdate: new RateLimitedFunc(function() { - /* eslint-disable babel/no-invalid-this */ - this.forceUpdate(); + this.setState({counters: this._computeCounters()}) }, 500), + _computeCounters: function() { + let counters = []; + + if (this.props.room && SettingsStore.isFeatureEnabled("feature_state_counters")) { + const stateEvs = this.props.room.currentState.getStateEvents('re.jki.counter'); + stateEvs.sort((a, b) => { + return a.getStateKey() < b.getStateKey(); + }); + + stateEvs.forEach((ev, idx) => { + const title = ev.getContent().title; + const value = ev.getContent().value; + const link = ev.getContent().link; + const severity = ev.getContent().severity || "normal"; + const stateKey = ev.getStateKey(); + + if (title && value && severity) { + counters.push({ + "title": title, + "value": value, + "link": link, + "severity": severity, + "stateKey": stateKey + }) + } + }); + } + + return counters; + }, + render: function() { const CallView = sdk.getComponent("voip.CallView"); const TintableSvg = sdk.getComponent("elements.TintableSvg"); @@ -165,17 +199,15 @@ module.exports = React.createClass({ />; let stateViews = null; - if (this.props.room && SettingsStore.isFeatureEnabled("feature_state_counters")) { - const stateEvs = this.props.room.currentState.getStateEvents('re.jki.counter'); - + if (this.state.counters && SettingsStore.isFeatureEnabled("feature_state_counters")) { let counters = []; - stateEvs.forEach((ev, idx) => { - const title = ev.getContent().title; - const value = ev.getContent().value; - const link = ev.getContent().link; - const severity = ev.getContent().severity || "normal"; - const stateKey = ev.getStateKey(); + this.state.counters.forEach((counter, idx) => { + const title = counter.title; + const value = counter.value; + const link = counter.link; + const severity = counter.severity; + const stateKey = counter.stateKey; if (title && value && severity) { let span = { title }: { value } From ab468b5346bcad41ec5cf7ccd9033c6f50dd123e Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 8 Jan 2019 11:21:19 +0000 Subject: [PATCH 45/93] Refactor travis-ci to use parallel jobs --- .travis.yml | 21 +++++++++++++++++-- scripts/{travis.sh => travis/install-deps.sh} | 6 ------ .../travis/test-riot.sh | 0 3 files changed, 19 insertions(+), 8 deletions(-) rename scripts/{travis.sh => travis/install-deps.sh} (58%) rename .travis-test-riot.sh => scripts/travis/test-riot.sh (100%) diff --git a/.travis.yml b/.travis.yml index 0def6d50f7..b52e18fbf1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,24 @@ addons: chrome: stable install: - npm install -# install synapse prerequisites for end to end tests - - sudo apt-get install build-essential python2.7-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev + - ./scripts/travis/install-deps.sh script: ./scripts/travis.sh + +matrix: + include: + - name: Linting Checks + script: + # run the linter, but exclude any files known to have errors or warnings. + - npm run lintwithexclusions + - name: End-to-End Tests + install: + - npm install + - ./scripts/travis/install-deps.sh + - sudo apt-get install build-essential python2.7-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev + script: + - ./scripts/travis/test-riot.sh + - name: Unit Tests + script: + - npm run test + diff --git a/scripts/travis.sh b/scripts/travis/install-deps.sh similarity index 58% rename from scripts/travis.sh rename to scripts/travis/install-deps.sh index 48410ea904..5acc801af8 100755 --- a/scripts/travis.sh +++ b/scripts/travis/install-deps.sh @@ -9,9 +9,3 @@ ln -s ../matrix-js-sdk node_modules/matrix-js-sdk cd matrix-js-sdk npm install cd .. - -npm run test -./.travis-test-riot.sh - -# run the linter, but exclude any files known to have errors or warnings. -npm run lintwithexclusions diff --git a/.travis-test-riot.sh b/scripts/travis/test-riot.sh similarity index 100% rename from .travis-test-riot.sh rename to scripts/travis/test-riot.sh From 025733244f49363acb8632aaf19203fe3e3d1d16 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 8 Jan 2019 11:47:49 +0000 Subject: [PATCH 46/93] Remove default script that is never called --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b52e18fbf1..b51f102cbf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,8 +16,6 @@ addons: install: - npm install - ./scripts/travis/install-deps.sh -script: - ./scripts/travis.sh matrix: include: From f9ecdcb480badc21facbea17bc0f71ed216fcdda Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 8 Jan 2019 11:48:11 +0000 Subject: [PATCH 47/93] Add npm cache --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index b51f102cbf..2cb3f4a9ad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,8 @@ install: - npm install - ./scripts/travis/install-deps.sh +cache: npm + matrix: include: - name: Linting Checks From ba5e3f86cb057c4f8cfe90e9ef5f2b0777e5fba7 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 8 Jan 2019 11:58:21 +0000 Subject: [PATCH 48/93] Revert "Add npm cache" This reverts commit f9ecdcb480badc21facbea17bc0f71ed216fcdda. Travis is unhappy about adding an NPM cache as it can't find matrix-js-sdk for some reason --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2cb3f4a9ad..b51f102cbf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,8 +17,6 @@ install: - npm install - ./scripts/travis/install-deps.sh -cache: npm - matrix: include: - name: Linting Checks From 52c5610660c5dcec972fed536e02677aadf73324 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 8 Jan 2019 14:35:06 +0100 Subject: [PATCH 49/93] update copy --- src/components/views/context_menus/TagTileContextMenu.js | 2 +- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/context_menus/TagTileContextMenu.js b/src/components/views/context_menus/TagTileContextMenu.js index ad93783485..de20f42a7b 100644 --- a/src/components/views/context_menus/TagTileContextMenu.js +++ b/src/components/views/context_menus/TagTileContextMenu.js @@ -74,7 +74,7 @@ export default class TagTileContextMenu extends React.Component { width="15" height="15" /> - { _t('View as grid') } + { _t('View as Grid') }
); } return
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4fab9ec438..8c544ee3b6 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1405,6 +1405,6 @@ "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room", - "View as grid": "View as grid", + "View as Grid": "View as Grid", "Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu": "Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu" } From 9e67dbf0086c822062c80b8031717d87f40c9324 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 8 Jan 2019 14:35:26 +0100 Subject: [PATCH 50/93] change icon --- res/img/icons-gridview.svg | 103 ------------------ .../views/context_menus/TagTileContextMenu.js | 2 +- 2 files changed, 1 insertion(+), 104 deletions(-) delete mode 100644 res/img/icons-gridview.svg diff --git a/res/img/icons-gridview.svg b/res/img/icons-gridview.svg deleted file mode 100644 index 862ca63765..0000000000 --- a/res/img/icons-gridview.svg +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - diff --git a/src/components/views/context_menus/TagTileContextMenu.js b/src/components/views/context_menus/TagTileContextMenu.js index de20f42a7b..8ce09bdb2c 100644 --- a/src/components/views/context_menus/TagTileContextMenu.js +++ b/src/components/views/context_menus/TagTileContextMenu.js @@ -70,7 +70,7 @@ export default class TagTileContextMenu extends React.Component { gridViewOption = (
From 02a83a7c2b53bb39e70737060a88e294b4679c81 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 7 Jan 2019 17:59:57 -0600 Subject: [PATCH 51/93] Use per-message string substitutions in key backup panel Including unused substitutions triggers console logs, so change the key backup panel to only substitute what's actually used in each message. Fixes https://github.com/vector-im/riot-web/issues/8047. --- .../views/settings/KeyBackupPanel.js | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 03b98d28a0..050c726ba4 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -155,46 +155,44 @@ export default class KeyBackupPanel extends React.Component { let backupSigStatuses = this.state.backupSigStatus.sigs.map((sig, i) => { const deviceName = sig.device.getDisplayName() || sig.device.deviceId; - const sigStatusSubstitutions = { - validity: sub => - - {sub} - , - verify: sub => - - {sub} - , - device: sub => {deviceName}, - }; + const validity = sub => + + {sub} + ; + const verify = sub => + + {sub} + ; + const device = sub => {deviceName}; let sigStatus; if (sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()) { sigStatus = _t( "Backup has a valid signature from this device", - {}, sigStatusSubstitutions, + {}, { validity }, ); } else if (sig.valid && sig.device.isVerified()) { sigStatus = _t( "Backup has a valid signature from " + "verified device ", - {}, sigStatusSubstitutions, + {}, { validity, verify, device }, ); } else if (sig.valid && !sig.device.isVerified()) { sigStatus = _t( "Backup has a valid signature from " + "unverified device ", - {}, sigStatusSubstitutions, + {}, { validity, verify, device }, ); } else if (!sig.valid && sig.device.isVerified()) { sigStatus = _t( "Backup has an invalid signature from " + "verified device ", - {}, sigStatusSubstitutions, + {}, { validity, verify, device }, ); } else if (!sig.valid && !sig.device.isVerified()) { sigStatus = _t( "Backup has an invalid signature from " + "unverified device ", - {}, sigStatusSubstitutions, + {}, { validity, verify, device }, ); } From 1fcafdab304d3603a4998068b7afaf53eee1cc93 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 8 Jan 2019 15:48:35 -0600 Subject: [PATCH 52/93] Allow New Recovery Dialog to be cancelled Fixes part of https://github.com/vector-im/riot-web/issues/8048. --- .../views/dialogs/keybackup/NewRecoveryMethodDialog.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js index e88e0444bc..6db6fe5c3e 100644 --- a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js +++ b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js @@ -79,7 +79,6 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {

{_t( From 254427461da694087279202f1f8781ef277a5166 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Jan 2019 12:48:13 +0100 Subject: [PATCH 53/93] fix PR feedback --- res/css/structures/_GroupGridView.scss | 10 ++++------ res/themes/dark/css/_dark.scss | 4 ++++ res/themes/dharma/css/_dharma.scss | 3 +++ res/themes/light/css/_base.scss | 4 ++++ src/components/structures/GroupGridView.js | 12 ++---------- src/components/views/rooms/MessageComposerInput.js | 3 --- src/i18n/strings/en_EN.json | 3 ++- src/stores/RoomViewStore.js | 4 ++-- 8 files changed, 21 insertions(+), 22 deletions(-) diff --git a/res/css/structures/_GroupGridView.scss b/res/css/structures/_GroupGridView.scss index 9e51af25f9..3a1ff165f1 100644 --- a/res/css/structures/_GroupGridView.scss +++ b/res/css/structures/_GroupGridView.scss @@ -26,7 +26,6 @@ limitations under the License. flex: 1 1 0; } - .mx_GroupGridView_rightPanel { display: flex; flex-direction: column; @@ -55,11 +54,11 @@ limitations under the License. display: flex; } -.mx_GroupGridView_emptyTile::before { +.mx_GroupGridView_emptyTile { display: block; margin-top: 100px; text-align: center; - content: "no room in this tile yet"; + user-select: none; } .mx_GroupGridView_tile { @@ -85,17 +84,16 @@ limitations under the License. .mx_GroupGridView_activeTile:before { border-radius: 14px; - border: 8px solid rgba(134, 193, 165, 0.5); + border: 8px solid $gridview-focus-border-glow-color; margin: -8px; } .mx_GroupGridView_activeTile:after { border-radius: 8px; - border: 2px solid rgba(134, 193, 165, 1); + border: 2px solid $gridview-focus-border-color; margin: -2px; } - .mx_GroupGridView_tile > .mx_RoomView { height: 100%; } diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 636db5b39e..10d512d576 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -162,6 +162,10 @@ $lightbox-bg-color: #454545; $lightbox-fg-color: #ffffff; $lightbox-border-color: #ffffff; +/*** GroupGridView ***/ +$gridview-focus-border-glow-color: rgba(134, 193, 165, 0.5); +$gridview-focus-border-color: rgba(134, 193, 165, 1); + $imagebody-giflabel: rgba(1, 1, 1, 0.7); $imagebody-giflabel-border: rgba(1, 1, 1, 0.2); diff --git a/res/themes/dharma/css/_dharma.scss b/res/themes/dharma/css/_dharma.scss index 08a287ad71..934e18b2a9 100644 --- a/res/themes/dharma/css/_dharma.scss +++ b/res/themes/dharma/css/_dharma.scss @@ -183,6 +183,9 @@ $lightbox-bg-color: #454545; $lightbox-fg-color: #ffffff; $lightbox-border-color: #ffffff; +/*** GroupGridView ***/ +$gridview-focus-border-glow-color: rgba(134, 193, 165, 0.5); +$gridview-focus-border-color: rgba(134, 193, 165, 1); // unused? $progressbar-color: #000; diff --git a/res/themes/light/css/_base.scss b/res/themes/light/css/_base.scss index 9fcb58d7f1..aaab5cd93a 100644 --- a/res/themes/light/css/_base.scss +++ b/res/themes/light/css/_base.scss @@ -175,6 +175,10 @@ $lightbox-bg-color: #454545; $lightbox-fg-color: #ffffff; $lightbox-border-color: #ffffff; +/*** GroupGridView ***/ +$gridview-focus-border-glow-color: rgba(134, 193, 165, 0.5); +$gridview-focus-border-color: rgba(134, 193, 165, 1); + $imagebody-giflabel: rgba(0, 0, 0, 0.7); $imagebody-giflabel-border: rgba(0, 0, 0, 0.2); diff --git a/src/components/structures/GroupGridView.js b/src/components/structures/GroupGridView.js index b5e511db41..a1a9e1b183 100644 --- a/src/components/structures/GroupGridView.js +++ b/src/components/structures/GroupGridView.js @@ -18,6 +18,7 @@ limitations under the License. import React from 'react'; import OpenRoomsStore from '../../stores/OpenRoomsStore'; import dis from '../../dispatcher'; +import {_t} from '../../languageHandler'; import RoomView from './RoomView'; import classNames from 'classnames'; import MainSplit from './MainSplit'; @@ -48,7 +49,6 @@ export default class RoomGridView extends React.Component { componentWillMount() { this._unmounted = false; this._openRoomsStoreRegistration = OpenRoomsStore.addListener(this.onRoomsChanged); - this._dispatcherRef = dis.register(this._onAction); } componentWillUnmount() { @@ -56,7 +56,6 @@ export default class RoomGridView extends React.Component { if (this._openRoomsStoreRegistration) { this._openRoomsStoreRegistration.remove(); } - dis.unregister(this._dispatcherRef); } onRoomsChanged() { @@ -67,13 +66,6 @@ export default class RoomGridView extends React.Component { }); } - _onAction(payload) { - switch (payload.action) { - default: - break; - } - } - _setActive(i) { const store = OpenRoomsStore.getRoomStoreAt(i); if (store !== this.state.activeRoomStore) { @@ -125,7 +117,7 @@ export default class RoomGridView extends React.Component { />

); } else { - return (
); + return (
{_t("No room in this tile yet.")}
); } }) }
diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 0740667242..4e7b4d3bbf 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -356,9 +356,6 @@ export default class MessageComposerInput extends React.Component { componentWillMount() { this.dispatcherRef = this.props.roomViewStore.getDispatcher().register(this.onAction); if (this.props.isGrid) { - - - this.historyManager = new NoopHistoryManager(); } else { this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_'); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8c544ee3b6..d6d70dfadd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1406,5 +1406,6 @@ "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room", "View as Grid": "View as Grid", - "Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu": "Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu" + "Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu": "Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu", + "No room in this tile yet.": "No room in this tile yet." } diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 02c76d05d9..a0b831ad17 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -307,6 +307,6 @@ export class RoomViewStore extends Store { } const MatrixDispatcher = require("../matrix-dispatcher"); -const blubber = new RoomViewStore(new MatrixDispatcher()); +const backwardsCompatInstance = new RoomViewStore(new MatrixDispatcher()); -export default blubber; +export default backwardsCompatInstance; From 79df843a6e5f70d18d53b0b4b738563a8e4f191d Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 9 Jan 2019 06:25:35 -0600 Subject: [PATCH 54/93] Fix path to New Recovery Method icon --- res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss b/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss index 370f82d9ab..f2ebe59925 100644 --- a/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss +++ b/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss @@ -24,7 +24,7 @@ limitations under the License. padding-bottom: 10px; &:before { - mask: url("../../../img/e2e/lock-warning.svg"); + mask: url("../../img/e2e/lock-warning.svg"); mask-repeat: no-repeat; background-color: $primary-fg-color; content: ""; From 5ce08523f82153b1429d71899e8f8c9f09bf351e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Jan 2019 14:21:45 +0100 Subject: [PATCH 55/93] split up script for unit and end-to-end tests so unit tests are not run as part of e2e tests --- .travis.yml | 9 ++------- scripts/travis/{test-riot.sh => build.sh} | 14 ------------- scripts/travis/end-to-end-tests.sh | 24 +++++++++++++++++++++++ scripts/travis/unit-tests.sh | 10 ++++++++++ 4 files changed, 36 insertions(+), 21 deletions(-) rename scripts/travis/{test-riot.sh => build.sh} (61%) mode change 100755 => 100644 create mode 100755 scripts/travis/end-to-end-tests.sh create mode 100644 scripts/travis/unit-tests.sh diff --git a/.travis.yml b/.travis.yml index b51f102cbf..2c414e7c59 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,9 +14,7 @@ node_js: addons: chrome: stable install: - - npm install - ./scripts/travis/install-deps.sh - matrix: include: - name: Linting Checks @@ -25,12 +23,9 @@ matrix: - npm run lintwithexclusions - name: End-to-End Tests install: - - npm install - - ./scripts/travis/install-deps.sh - sudo apt-get install build-essential python2.7-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev script: - - ./scripts/travis/test-riot.sh + - ./scripts/travis/end-to-end-tests.sh - name: Unit Tests script: - - npm run test - + - ./scripts/travis/unit-tests.sh diff --git a/scripts/travis/test-riot.sh b/scripts/travis/build.sh old mode 100755 new mode 100644 similarity index 61% rename from scripts/travis/test-riot.sh rename to scripts/travis/build.sh index d1c2804b2a..a353e38a06 --- a/scripts/travis/test-riot.sh +++ b/scripts/travis/build.sh @@ -24,18 +24,4 @@ rm -r node_modules/matrix-react-sdk ln -s "$REACT_SDK_DIR" node_modules/matrix-react-sdk npm run build -npm run test popd - -if [ "$TRAVIS_BRANCH" = "develop" ] -then - # run end to end tests - scripts/fetchdep.sh matrix-org matrix-react-end-to-end-tests master - pushd matrix-react-end-to-end-tests - ln -s $REACT_SDK_DIR/$RIOT_WEB_DIR riot/riot-web - # PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh - # CHROME_PATH=$(which google-chrome-stable) ./run.sh - ./install.sh - ./run.sh --travis - popd -fi diff --git a/scripts/travis/end-to-end-tests.sh b/scripts/travis/end-to-end-tests.sh new file mode 100755 index 0000000000..285458bd4b --- /dev/null +++ b/scripts/travis/end-to-end-tests.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# +# script which is run by the travis build (after `npm run test`). +# +# clones riot-web develop and runs the tests against our version of react-sdk. + +set -ev + +RIOT_WEB_DIR=riot-web +REACT_SDK_DIR=`pwd` + +if [ "$TRAVIS_BRANCH" = "develop" ] +then + scripts/travis/build.sh + # run end to end tests + scripts/fetchdep.sh matrix-org matrix-react-end-to-end-tests master + pushd matrix-react-end-to-end-tests + ln -s $REACT_SDK_DIR/$RIOT_WEB_DIR riot/riot-web + # PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh + # CHROME_PATH=$(which google-chrome-stable) ./run.sh + ./install.sh + ./run.sh --travis + popd +fi diff --git a/scripts/travis/unit-tests.sh b/scripts/travis/unit-tests.sh new file mode 100644 index 0000000000..a8e0a63b31 --- /dev/null +++ b/scripts/travis/unit-tests.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# +# script which is run by the travis build (after `npm run test`). +# +# clones riot-web develop and runs the tests against our version of react-sdk. + +set -ev + +scripts/travis/build.sh +npm run test From 481cd292adb292894d62aa6adee52cb42117cefd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Jan 2019 14:36:32 +0100 Subject: [PATCH 56/93] make e2e tests job conditional at travis.yml level --- .travis.yml | 1 + scripts/travis/end-to-end-tests.sh | 23 ++++++++++------------- scripts/travis/install-deps.sh | 2 +- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2c414e7c59..bfc4d265a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,6 +22,7 @@ matrix: # run the linter, but exclude any files known to have errors or warnings. - npm run lintwithexclusions - name: End-to-End Tests + if: branch = develop install: - sudo apt-get install build-essential python2.7-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev script: diff --git a/scripts/travis/end-to-end-tests.sh b/scripts/travis/end-to-end-tests.sh index 285458bd4b..361b053d2b 100755 --- a/scripts/travis/end-to-end-tests.sh +++ b/scripts/travis/end-to-end-tests.sh @@ -9,16 +9,13 @@ set -ev RIOT_WEB_DIR=riot-web REACT_SDK_DIR=`pwd` -if [ "$TRAVIS_BRANCH" = "develop" ] -then - scripts/travis/build.sh - # run end to end tests - scripts/fetchdep.sh matrix-org matrix-react-end-to-end-tests master - pushd matrix-react-end-to-end-tests - ln -s $REACT_SDK_DIR/$RIOT_WEB_DIR riot/riot-web - # PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh - # CHROME_PATH=$(which google-chrome-stable) ./run.sh - ./install.sh - ./run.sh --travis - popd -fi +scripts/travis/build.sh +# run end to end tests +scripts/fetchdep.sh matrix-org matrix-react-end-to-end-tests master +pushd matrix-react-end-to-end-tests +ln -s $REACT_SDK_DIR/$RIOT_WEB_DIR riot/riot-web +# PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh +# CHROME_PATH=$(which google-chrome-stable) ./run.sh +./install.sh +./run.sh --travis +popd diff --git a/scripts/travis/install-deps.sh b/scripts/travis/install-deps.sh index 5acc801af8..04cd728157 100755 --- a/scripts/travis/install-deps.sh +++ b/scripts/travis/install-deps.sh @@ -1,7 +1,7 @@ #!/bin/sh set -ex - +npm install scripts/fetchdep.sh matrix-org matrix-js-sdk rm -r node_modules/matrix-js-sdk || true ln -s ../matrix-js-sdk node_modules/matrix-js-sdk From 419726f4231704b8cc976e177937baedf80c2b01 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Jan 2019 14:43:05 +0100 Subject: [PATCH 57/93] set executable perms --- scripts/travis/build.sh | 0 scripts/travis/unit-tests.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/travis/build.sh mode change 100644 => 100755 scripts/travis/unit-tests.sh diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh old mode 100644 new mode 100755 diff --git a/scripts/travis/unit-tests.sh b/scripts/travis/unit-tests.sh old mode 100644 new mode 100755 From 509ae4cea4d7c5ff8654089c528965876c0449a5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Jan 2019 15:02:20 +0100 Subject: [PATCH 58/93] run unit tests on riot-web like before --- scripts/travis/unit-tests.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/travis/unit-tests.sh b/scripts/travis/unit-tests.sh index a8e0a63b31..a2f0d61112 100755 --- a/scripts/travis/unit-tests.sh +++ b/scripts/travis/unit-tests.sh @@ -6,5 +6,9 @@ set -ev +RIOT_WEB_DIR=riot-web + scripts/travis/build.sh +pushd "$RIOT_WEB_DIR" npm run test +popd From 45558f5323277763f9239a128138f5ac0a790227 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Jan 2019 15:16:20 +0100 Subject: [PATCH 59/93] run both react-sdk and riot-web tests --- .travis.yml | 3 +++ scripts/travis/riot-unit-tests.sh | 14 ++++++++++++++ scripts/travis/unit-tests.sh | 4 ---- 3 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 scripts/travis/riot-unit-tests.sh diff --git a/.travis.yml b/.travis.yml index bfc4d265a4..0746cc0dff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,3 +30,6 @@ matrix: - name: Unit Tests script: - ./scripts/travis/unit-tests.sh + - name: Riot-web Unit Tests + script: + - ./scripts/travis/riot-unit-tests.sh diff --git a/scripts/travis/riot-unit-tests.sh b/scripts/travis/riot-unit-tests.sh new file mode 100644 index 0000000000..a2f0d61112 --- /dev/null +++ b/scripts/travis/riot-unit-tests.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# +# script which is run by the travis build (after `npm run test`). +# +# clones riot-web develop and runs the tests against our version of react-sdk. + +set -ev + +RIOT_WEB_DIR=riot-web + +scripts/travis/build.sh +pushd "$RIOT_WEB_DIR" +npm run test +popd diff --git a/scripts/travis/unit-tests.sh b/scripts/travis/unit-tests.sh index a2f0d61112..a8e0a63b31 100755 --- a/scripts/travis/unit-tests.sh +++ b/scripts/travis/unit-tests.sh @@ -6,9 +6,5 @@ set -ev -RIOT_WEB_DIR=riot-web - scripts/travis/build.sh -pushd "$RIOT_WEB_DIR" npm run test -popd From 19190deb3c9e1c556eff26da492a588a1ff6bd85 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Jan 2019 15:20:55 +0100 Subject: [PATCH 60/93] set x perms --- scripts/travis/riot-unit-tests.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/travis/riot-unit-tests.sh diff --git a/scripts/travis/riot-unit-tests.sh b/scripts/travis/riot-unit-tests.sh old mode 100644 new mode 100755 From 7381879e3aa5d4e695c07f805d4e6dc96231212b Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 9 Jan 2019 16:27:41 +0000 Subject: [PATCH 61/93] Fix a few things with cancelling recovery reminder * Put a cancel button on the first page of the create key backup dialog * Move settings logic into the room reminder: I know this is moving logic *into* a view but RoomView is quite heavyweight as it is. * Give the recovery reminder an explicit 'onDontAskAgainSet' rather than onFinished which was getting called with false when the last screen of the dialog was closed with the cancel 'x' rather than the 'close' button. https://github.com/vector-im/riot-web/issues/8066 --- .../keybackup/CreateKeyBackupDialog.js | 4 +-- src/components/structures/RoomView.js | 19 +++++--------- .../views/rooms/RoomRecoveryReminder.js | 26 ++++++++++++------- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index c593a9b3ea..64e0702012 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -1,5 +1,5 @@ /* -Copyright 2018 New Vector Ltd +Copyright 2018, 2019 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. @@ -503,7 +503,7 @@ export default React.createClass({
{content} diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 2ee4d8e596..3cb90dc799 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1,7 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd -Copyright 2018 New Vector Ltd +Copyright 2018, 2019 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. @@ -611,17 +611,10 @@ module.exports = React.createClass({ } }, - async onRoomRecoveryReminderFinished(backupCreated) { - // If the user cancelled the key backup dialog, it suggests they don't - // want to be reminded anymore. - if (!backupCreated) { - await SettingsStore.setValue( - "showRoomRecoveryReminder", - null, - SettingLevel.ACCOUNT, - false, - ); - } + onRoomRecoveryReminderDontAskAgain: function() { + // Called when the option to not ask again is set: + // force an update to hide the recovery reminder + this.forceUpdate(); }, onKeyBackupStatus() { @@ -1704,7 +1697,7 @@ module.exports = React.createClass({ aux = ; hideCancel = true; } else if (showRoomRecoveryReminder) { - aux = ; + aux = ; hideCancel = true; } else if (this.state.showingPinned) { hideCancel = true; // has own cancel diff --git a/src/components/views/rooms/RoomRecoveryReminder.js b/src/components/views/rooms/RoomRecoveryReminder.js index d03c5fc96d..01447012e6 100644 --- a/src/components/views/rooms/RoomRecoveryReminder.js +++ b/src/components/views/rooms/RoomRecoveryReminder.js @@ -1,5 +1,5 @@ /* -Copyright 2018 New Vector Ltd +Copyright 2018, 2019 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. @@ -20,10 +20,16 @@ import sdk from "../../../index"; import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; import MatrixClientPeg from "../../../MatrixClientPeg"; +import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; export default class RoomRecoveryReminder extends React.PureComponent { static propTypes = { - onFinished: PropTypes.func.isRequired, + // called if the user sets the option to suppress this reminder in the future + onDontAskAgainSet: PropTypes.func, + } + + static defaultProps = { + onDontAskAgainSet: function() {}, } constructor(props) { @@ -82,7 +88,6 @@ export default class RoomRecoveryReminder extends React.PureComponent { Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, { userId: MatrixClientPeg.get().credentials.userId, device: this.state.unverifiedDevice, - onFinished: this.props.onFinished, }); return; } @@ -91,9 +96,6 @@ export default class RoomRecoveryReminder extends React.PureComponent { // we'll show the create key backup flow. Modal.createTrackedDialogAsync("Key Backup", "Key Backup", import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"), - { - onFinished: this.props.onFinished, - }, ); } @@ -103,10 +105,14 @@ export default class RoomRecoveryReminder extends React.PureComponent { Modal.createTrackedDialogAsync("Ignore Recovery Reminder", "Ignore Recovery Reminder", import("../../../async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog"), { - onDontAskAgain: () => { - // Report false to the caller, who should prevent the - // reminder from appearing in the future. - this.props.onFinished(false); + onDontAskAgain: async () => { + await SettingsStore.setValue( + "showRoomRecoveryReminder", + null, + SettingLevel.ACCOUNT, + false, + ); + this.props.onDontAskAgainSet(); }, onSetup: () => { this.showSetupDialog(); From 91a7c146e44752ad855c860588def52cc4218aa3 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 8 Jan 2019 10:54:38 -0600 Subject: [PATCH 62/93] Fix lint warning in create key backup --- .../views/dialogs/keybackup/CreateKeyBackupDialog.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index c593a9b3ea..1be969bf8b 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -344,7 +344,10 @@ export default React.createClass({ _renderPhaseShowKey: function() { let bodyText; if (this.state.setPassPhrase) { - bodyText = _t("As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase."); + bodyText = _t( + "As a safety net, you can use it to restore your encrypted message " + + "history if you forget your Recovery Passphrase.", + ); } else { bodyText = _t("As a safety net, you can use it to restore your encrypted message history."); } From 2bfe6e7500fbef6994211d2bf1088949584cf0e4 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 8 Jan 2019 10:55:01 -0600 Subject: [PATCH 63/93] Fix React nesting error in create key backup --- .../views/dialogs/keybackup/CreateKeyBackupDialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 1be969bf8b..be728916dd 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -355,7 +355,7 @@ export default React.createClass({ return

{_t("Make a copy of this Recovery Key and keep it safe.")}

{bodyText}

-

+

{_t("Your Recovery Key")}
@@ -372,7 +372,7 @@ export default React.createClass({
-

+
; }, From 7dbc970347f5ee738f8ddb7de4e7aa99e8827b0b Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 8 Jan 2019 13:57:22 -0600 Subject: [PATCH 64/93] Mark KeyBackupPanel as a pure component KeyBackupPanel depends only on its own state and its children are pure, so it can be pure as well. This avoids some unnecessary re-renders. --- src/components/views/settings/KeyBackupPanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 050c726ba4..1448b9d239 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -21,7 +21,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; -export default class KeyBackupPanel extends React.Component { +export default class KeyBackupPanel extends React.PureComponent { constructor(props) { super(props); From 731f9ee7dfe1a0e901c74ff1f9722a8001d84332 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 8 Jan 2019 18:05:48 -0600 Subject: [PATCH 65/93] Display key backup upload progress in Settings This adds a summary of the keys currently waiting for backup, which may be useful for following a large upload as it progresses. --- .../views/settings/KeyBackupPanel.js | 28 +++++++++++++++++++ src/i18n/strings/en_EN.json | 2 ++ 2 files changed, 30 insertions(+) diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 1448b9d239..8aa50fbdcf 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -28,6 +28,8 @@ export default class KeyBackupPanel extends React.PureComponent { this._startNewBackup = this._startNewBackup.bind(this); this._deleteBackup = this._deleteBackup.bind(this); this._verifyDevice = this._verifyDevice.bind(this); + this._onKeyBackupSessionsRemaining = + this._onKeyBackupSessionsRemaining.bind(this); this._onKeyBackupStatus = this._onKeyBackupStatus.bind(this); this._restoreBackup = this._restoreBackup.bind(this); @@ -36,6 +38,7 @@ export default class KeyBackupPanel extends React.PureComponent { loading: true, error: null, backupInfo: null, + sessionsRemaining: 0, }; } @@ -43,6 +46,10 @@ export default class KeyBackupPanel extends React.PureComponent { this._loadBackupStatus(); MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatus); + MatrixClientPeg.get().on( + 'crypto.keyBackupSessionsRemaining', + this._onKeyBackupSessionsRemaining, + ); } componentWillUnmount() { @@ -50,9 +57,19 @@ export default class KeyBackupPanel extends React.PureComponent { if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatus); + MatrixClientPeg.get().removeListener( + 'crypto.keyBackupSessionsRemaining', + this._onKeyBackupSessionsRemaining, + ); } } + _onKeyBackupSessionsRemaining(sessionsRemaining) { + this.setState({ + sessionsRemaining, + }); + } + _onKeyBackupStatus() { this._loadBackupStatus(); } @@ -153,6 +170,16 @@ export default class KeyBackupPanel extends React.PureComponent { ); } + let uploadStatus; + const { sessionsRemaining } = this.state; + if (sessionsRemaining > 0) { + uploadStatus = _t("Backing up %(sessionsRemaining)s keys...", { + sessionsRemaining, + }); + } else { + uploadStatus = _t("All keys backed up"); + } + let backupSigStatuses = this.state.backupSigStatus.sigs.map((sig, i) => { const deviceName = sig.device.getDisplayName() || sig.device.deviceId; const validity = sub => @@ -217,6 +244,7 @@ export default class KeyBackupPanel extends React.PureComponent { {_t("Backup version: ")}{this.state.backupInfo.version}
{_t("Algorithm: ")}{this.state.backupInfo.algorithm}
{clientBackupStatus}
+ {uploadStatus}
{backupSigStatuses}


not uploading keys to this backup": "This device is not uploading keys to this backup", + "Backing up %(sessionsRemaining)s keys...": "Backing up %(sessionsRemaining)s keys...", + "All keys backed up": "All keys backed up", "Backup has a valid signature from this device": "Backup has a valid signature from this device", "Backup has a valid signature from verified device ": "Backup has a valid signature from verified device ", "Backup has a valid signature from unverified device ": "Backup has a valid signature from unverified device ", From 365a7273d80a5aad91a0593b2499786da997b126 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 9 Jan 2019 05:24:15 -0600 Subject: [PATCH 66/93] Move initial key backup to background Since the initial key backup can take several minutes for some users, this moves the upload step to the background. The create key backup flow now only marks all sessions for backup synchronously, with the actual backup happening later. The key backup panel in Settings gains a new row to show a summary of upload status. Users are directed there if they wish to know if the backup is done. The text in various related dialogs has also been tweaked to fit the new flow. --- .../keybackup/CreateKeyBackupDialog.js | 17 ++++++++------- .../views/settings/KeyBackupPanel.js | 21 ++++++++++++------- src/i18n/strings/en_EN.json | 10 ++++----- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index be728916dd..89a9e9e47d 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -21,7 +21,7 @@ import { scorePassword } from '../../../../utils/PasswordScorer'; import FileSaver from 'file-saver'; -import { _t, _td } from '../../../../languageHandler'; +import { _t } from '../../../../languageHandler'; const PHASE_PASSPHRASE = 0; const PHASE_PASSPHRASE_CONFIRM = 1; @@ -102,7 +102,7 @@ export default React.createClass({ info = await MatrixClientPeg.get().createKeyBackupVersion( this._keyBackupInfo, ); - await MatrixClientPeg.get().backupAllGroupSessions(info.version); + await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup(); this.setState({ phase: PHASE_DONE, }); @@ -408,7 +408,6 @@ export default React.createClass({ _renderBusyPhase: function(text) { const Spinner = sdk.getComponent('views.elements.Spinner'); return
-

{_t(text)}

; }, @@ -416,8 +415,10 @@ export default React.createClass({ _renderPhaseDone: function() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return
-

{_t("Backup created")}

-

{_t("Your encryption keys are now being backed up to your Homeserver.")}

+

{_t( + "Your encryption keys are now being backed up in the background " + + "to your Homeserver. The initial backup could take several minutes. " + + "You can view key backup upload progress in Settings.")}

not uploading keys to this backup", {}, + "This device is not using key backup", {}, {b: x => {x}}, ); } let uploadStatus; const { sessionsRemaining } = this.state; - if (sessionsRemaining > 0) { - uploadStatus = _t("Backing up %(sessionsRemaining)s keys...", { - sessionsRemaining, - }); + if (!MatrixClientPeg.get().getKeyBackupEnabled()) { + // No upload status to show when backup disabled. + uploadStatus = ""; + } else if (sessionsRemaining > 0) { + uploadStatus =
+ {_t("Backing up %(sessionsRemaining)s keys...", { sessionsRemaining })}
+
; } else { - uploadStatus = _t("All keys backed up"); + uploadStatus =
+ {_t("All keys backed up")}
+
; } let backupSigStatuses = this.state.backupSigStatus.sigs.map((sig, i) => { @@ -244,7 +249,7 @@ export default class KeyBackupPanel extends React.PureComponent { {_t("Backup version: ")}{this.state.backupInfo.version}
{_t("Algorithm: ")}{this.state.backupInfo.algorithm}
{clientBackupStatus}
- {uploadStatus}
+ {uploadStatus}
{backupSigStatuses}


not uploading keys to this backup": "This device is not uploading keys to this backup", + "This device is using key backup": "This device is using key backup", + "This device is not using key backup": "This device is not using key backup", "Backing up %(sessionsRemaining)s keys...": "Backing up %(sessionsRemaining)s keys...", "All keys backed up": "All keys backed up", "Backup has a valid signature from this device": "Backup has a valid signature from this device", @@ -1388,15 +1388,15 @@ "Print it and store it somewhere safe": "Print it and store it somewhere safe", "Save it on a USB key or backup drive": "Save it on a USB key or backup drive", "Copy it to your personal cloud storage": "Copy it to your personal cloud storage", - "Backup created": "Backup created", - "Your encryption keys are now being backed up to your Homeserver.": "Your encryption keys are now being backed up to your Homeserver.", + "Your encryption keys are now being backed up in the background to your Homeserver. The initial backup could take several minutes. You can view key backup upload progress in Settings.": "Your encryption keys are now being backed up in the background to your Homeserver. The initial backup could take several minutes. You can view key backup upload progress in Settings.", "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.", "Set up Secure Message Recovery": "Set up Secure Message Recovery", "Create a Recovery Passphrase": "Create a Recovery Passphrase", "Confirm Recovery Passphrase": "Confirm Recovery Passphrase", "Recovery Key": "Recovery Key", "Keep it safe": "Keep it safe", - "Backing up...": "Backing up...", + "Starting backup...": "Starting backup...", + "Backup Started": "Backup Started", "Create Key Backup": "Create Key Backup", "Unable to create key backup": "Unable to create key backup", "Retry": "Retry", From 40e8e48e08d175d9714e28fb08e2f2a8ee823079 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Jan 2019 19:05:21 +0100 Subject: [PATCH 67/93] fix grid growing wider than viewport --- res/css/structures/_GroupGridView.scss | 1 + res/css/structures/_MatrixChat.scss | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/res/css/structures/_GroupGridView.scss b/res/css/structures/_GroupGridView.scss index 3a1ff165f1..541052175d 100644 --- a/res/css/structures/_GroupGridView.scss +++ b/res/css/structures/_GroupGridView.scss @@ -24,6 +24,7 @@ limitations under the License. grid-template-columns: repeat(3, calc(100% / 3)); grid-template-rows: repeat(2, calc(100% / 2)); flex: 1 1 0; + min-width: 0; } .mx_GroupGridView_rightPanel { diff --git a/res/css/structures/_MatrixChat.scss b/res/css/structures/_MatrixChat.scss index a843bb7fee..6d8b79ecb2 100644 --- a/res/css/structures/_MatrixChat.scss +++ b/res/css/structures/_MatrixChat.scss @@ -73,7 +73,8 @@ limitations under the License. .mx_MatrixChat > :not(.mx_LeftPanel_container):not(.mx_ResizeHandle) { background-color: $primary-bg-color; - flex: 1; + flex: 1 1 0; + min-width: 0; /* Experimental fix for https://github.com/vector-im/vector-web/issues/947 and https://github.com/vector-im/vector-web/issues/946. From 8e4d8ccca7a18363ea16fc0a4e985dc00b81ebf9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 9 Jan 2019 18:10:35 +0000 Subject: [PATCH 68/93] Add spaces back to async arrow functions As per https://github.com/matrix-org/matrix-js-sdk/pull/821 Requires https://github.com/matrix-org/matrix-js-sdk/pull/821 --- package.json | 4 ++-- .../views/dialogs/keybackup/NewRecoveryMethodDialog.js | 2 +- src/autocomplete/CommunityProvider.js | 2 +- src/components/structures/GroupView.js | 2 +- .../views/context_menus/GroupInviteTileContextMenu.js | 2 +- .../views/context_menus/StatusMessageContextMenu.js | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 155d3d1b23..1b628067fc 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "start:init": "babel src -d lib --source-maps --copy-files", "lint": "eslint src/", "lintall": "eslint src/ test/", - "lintwithexclusions": "eslint --max-warnings 16 --ignore-path .eslintignore.errorfiles src test", + "lintwithexclusions": "eslint --max-warnings 14 --ignore-path .eslintignore.errorfiles src test", "clean": "rimraf lib", "prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt", "test": "karma start --single-run=true --browsers ChromeHeadless", @@ -118,7 +118,7 @@ "babel-preset-react": "^6.24.1", "chokidar": "^1.6.1", "concurrently": "^4.0.1", - "eslint": "^5.8.0", + "eslint": "^5.12.0", "eslint-config-google": "^0.7.1", "eslint-plugin-babel": "^5.2.1", "eslint-plugin-flowtype": "^2.30.0", diff --git a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js index 6db6fe5c3e..c97ce58c07 100644 --- a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js +++ b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js @@ -32,7 +32,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent { dis.dispatch({ action: 'view_user_settings' }); } - onSetupClick = async() => { + onSetupClick = async () => { // TODO: Should change to a restore key backup flow that checks the // recovery passphrase while at the same time also cross-signing the // device as well in a single flow. Since we don't have that yet, we'll diff --git a/src/autocomplete/CommunityProvider.js b/src/autocomplete/CommunityProvider.js index b85c09b320..d164fab46a 100644 --- a/src/autocomplete/CommunityProvider.js +++ b/src/autocomplete/CommunityProvider.js @@ -61,7 +61,7 @@ export default class CommunityProvider extends AutocompleteProvider { if (command) { const joinedGroups = cli.getGroups().filter(({myMembership}) => myMembership === 'join'); - const groups = (await Promise.all(joinedGroups.map(async({groupId}) => { + const groups = (await Promise.all(joinedGroups.map(async ({groupId}) => { try { return FlairStore.getGroupProfileCached(cli, groupId); } catch (e) { // if FlairStore failed, fall back to just groupId diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 937e07d31e..834fcd2340 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -781,7 +781,7 @@ export default React.createClass({ ), button: _t("Leave"), danger: this.state.isUserPrivileged, - onFinished: async(confirmed) => { + onFinished: async (confirmed) => { if (!confirmed) return; this.setState({membershipBusy: true}); diff --git a/src/components/views/context_menus/GroupInviteTileContextMenu.js b/src/components/views/context_menus/GroupInviteTileContextMenu.js index 2dd611843a..e30acca16d 100644 --- a/src/components/views/context_menus/GroupInviteTileContextMenu.js +++ b/src/components/views/context_menus/GroupInviteTileContextMenu.js @@ -48,7 +48,7 @@ export default class GroupInviteTileContextMenu extends React.Component { Modal.createTrackedDialog('Reject community invite', '', QuestionDialog, { title: _t('Reject invitation'), description: _t('Are you sure you want to reject the invitation?'), - onFinished: async(shouldLeave) => { + onFinished: async (shouldLeave) => { if (!shouldLeave) return; // FIXME: controller shouldn't be loading a view :( diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js index f07220db44..d062fc2a3e 100644 --- a/src/components/views/context_menus/StatusMessageContextMenu.js +++ b/src/components/views/context_menus/StatusMessageContextMenu.js @@ -35,7 +35,7 @@ export default class StatusMessageContextMenu extends React.Component { }; } - _onClearClick = async(e) => { + _onClearClick = async (e) => { await MatrixClientPeg.get()._unstable_setStatusMessage(""); this.setState({message: ""}); }; From f269b5fbbcb08cde6232d89821ef2767fcdf7aa4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 9 Jan 2019 19:14:30 +0000 Subject: [PATCH 69/93] Fix spaceless async in ignored files too --- .eslintignore.errorfiles | 1 - package.json | 2 +- src/components/structures/UserSettings.js | 2 +- src/components/views/rooms/MemberInfo.js | 2 +- src/components/views/rooms/MessageComposerInput.js | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index 0b4266c0b5..1a12432c87 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -18,7 +18,6 @@ src/components/structures/ScrollPanel.js src/components/structures/SearchBox.js src/components/structures/TimelinePanel.js src/components/structures/UploadBar.js -src/components/structures/UserSettings.js src/components/views/avatars/BaseAvatar.js src/components/views/avatars/MemberAvatar.js src/components/views/create_room/RoomAlias.js diff --git a/package.json b/package.json index 1b628067fc..8804c0911b 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "start:init": "babel src -d lib --source-maps --copy-files", "lint": "eslint src/", "lintall": "eslint src/ test/", - "lintwithexclusions": "eslint --max-warnings 14 --ignore-path .eslintignore.errorfiles src test", + "lintwithexclusions": "eslint --max-warnings 19 --ignore-path .eslintignore.errorfiles src test", "clean": "rimraf lib", "prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt", "test": "karma start --single-run=true --browsers ChromeHeadless", diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index bb31510cf6..02b94b408d 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -835,7 +835,7 @@ module.exports = React.createClass({ SettingsStore.getLabsFeatures().forEach((featureId) => { // TODO: this ought to be a separate component so that we don't need // to rebind the onChange each time we render - const onChange = async(e) => { + const onChange = async (e) => { const checked = e.target.checked; if (featureId === "feature_lazyloading") { const confirmed = await this._onLazyLoadChanging(checked); diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 226adb910f..2b50ff5e48 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -714,7 +714,7 @@ module.exports = withMatrixClient(React.createClass({ if (!member || !member.membership || member.membership === 'leave') { const roomId = member && member.roomId ? member.roomId : this.props.roomId; - const onInviteUserButton = async() => { + const onInviteUserButton = async () => { try { // We use a MultiInviter to re-use the invite logic, even though // we're only inviting one user. diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 4e7b4d3bbf..f39d7aad6e 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -1274,7 +1274,7 @@ export default class MessageComposerInput extends React.Component { } }; - selectHistory = async(up) => { + selectHistory = async (up) => { const delta = up ? -1 : 1; // True if we are not currently selecting history, but composing a message From b9719abbc2da9813bd3ece940a4d1ddd7beaf4a9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 9 Jan 2019 19:36:34 +0000 Subject: [PATCH 70/93] Oops, missed some asyncs --- src/components/views/rooms/MessageComposerInput.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index f39d7aad6e..80f90d37b4 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -1322,7 +1322,7 @@ export default class MessageComposerInput extends React.Component { return true; }; - onTab = async(e) => { + onTab = async (e) => { this.setState({ someCompletions: null, }); @@ -1344,7 +1344,7 @@ export default class MessageComposerInput extends React.Component { up ? this.autocomplete.onUpArrow() : this.autocomplete.onDownArrow(); }; - onEscape = async(e) => { + onEscape = async (e) => { e.preventDefault(); if (this.autocomplete) { this.autocomplete.onEscape(e); @@ -1363,7 +1363,7 @@ export default class MessageComposerInput extends React.Component { /* If passed null, restores the original editor content from state.originalEditorState. * If passed a non-null displayedCompletion, modifies state.originalEditorState to compute new state.editorState. */ - setDisplayedCompletion = async(displayedCompletion: ?Completion): boolean => { + setDisplayedCompletion = async (displayedCompletion: ?Completion): boolean => { const activeEditorState = this.state.originalEditorState || this.state.editorState; if (displayedCompletion == null) { From 9903c61a21bb7cbbc3dd52b30ebbd55ff56f7051 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 10 Jan 2019 09:41:26 +0000 Subject: [PATCH 71/93] Fix lint errors in MessageComposerInput and remove from ignored files Missed a load of the async arrows functions when changing them because they were in the ignored files, so trying to chip away at this. A lot of these were unused imports / variables. Probably the only interesting one was a 'this' in a non-member function which I've moved to be a member function. --- .eslintignore.errorfiles | 1 - .../views/rooms/MessageComposerInput.js | 52 +++++++++---------- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index 1a12432c87..1c9e2d4413 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -53,7 +53,6 @@ src/components/views/rooms/MemberInfo.js src/components/views/rooms/MemberList.js src/components/views/rooms/MemberTile.js src/components/views/rooms/MessageComposer.js -src/components/views/rooms/MessageComposerInput.js src/components/views/rooms/PinnedEventTile.js src/components/views/rooms/RoomList.js src/components/views/rooms/RoomPreviewBar.js diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 80f90d37b4..4c800ad8d2 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -15,13 +15,11 @@ 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'; -import type SyntheticKeyboardEvent from 'react/lib/SyntheticKeyboardEvent'; import { Editor } from 'slate-react'; import { getEventTransfer } from 'slate-react'; -import { Value, Document, Block, Inline, Text, Range, Node } from 'slate'; +import { Value, Block, Inline, Range } from 'slate'; import type { Change } from 'slate'; import Html from 'slate-html-serializer'; @@ -30,7 +28,6 @@ import Plain from 'slate-plain-serializer'; import PlainWithPillsSerializer from "../../../autocomplete/PlainWithPillsSerializer"; import classNames from 'classnames'; -import Promise from 'bluebird'; import MatrixClientPeg from '../../../MatrixClientPeg'; import type {MatrixClient} from 'matrix-js-sdk/lib/matrix'; @@ -38,7 +35,7 @@ import {processCommandInput} from '../../../SlashCommands'; import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard'; import Modal from '../../../Modal'; import sdk from '../../../index'; -import { _t, _td } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; import Analytics from '../../../Analytics'; import * as RichText from '../../../RichText'; @@ -49,27 +46,24 @@ import Markdown from '../../../Markdown'; import ComposerHistoryManager from '../../../ComposerHistoryManager'; import MessageComposerStore from '../../../stores/MessageComposerStore'; -import {MATRIXTO_MD_LINK_PATTERN, MATRIXTO_URL_PATTERN} from '../../../linkify-matrix'; -const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g'); +import {MATRIXTO_URL_PATTERN} from '../../../linkify-matrix'; -import {asciiRegexp, unicodeRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort, toShort} from 'emojione'; +import { + asciiRegexp, unicodeRegexp, shortnameToUnicode, + asciiList, mapUnicodeToShort, toShort, +} from 'emojione'; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; import {makeUserPermalink} from "../../../matrix-to"; import ReplyPreview from "./ReplyPreview"; import ReplyThread from "../elements/ReplyThread"; import {ContentHelpers} from 'matrix-js-sdk'; -const EMOJI_SHORTNAMES = Object.keys(emojioneList); const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort(); const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$'); const EMOJI_REGEX = new RegExp(unicodeRegexp, 'g'); const TYPING_USER_TIMEOUT = 10000; const TYPING_SERVER_TIMEOUT = 30000; -const ENTITY_TYPES = { - AT_ROOM_PILL: 'ATROOMPILL', -}; - // the Slate node type to default to for unstyled text const DEFAULT_NODE = 'paragraph'; @@ -114,15 +108,6 @@ const SLATE_SCHEMA = { }, }; -function onSendMessageFailed(err, room) { - // XXX: temporary logging to try to diagnose - // https://github.com/vector-im/riot-web/issues/3148 - console.log('MessageComposer got send failure: ' + err.name + '('+err+')'); - this.props.roomViewStore.getDispatcher().dispatch({ - action: 'message_send_failed', - }); -} - function rangeEquals(a: Range, b: Range): boolean { return (a.anchor.key === b.anchor.key && a.anchor.offset === b.anchorOffset @@ -370,8 +355,16 @@ export default class MessageComposerInput extends React.Component { this._editor = e; } + onSendMessageFailed = (err, room) => { + // XXX: temporary logging to try to diagnose + // https://github.com/vector-im/riot-web/issues/3148 + console.log('MessageComposer got send failure: ' + err.name + '('+err+')'); + this.props.roomViewStore.getDispatcher().dispatch({ + action: 'message_send_failed', + }); + } + onAction = (payload) => { - const editor = this._editor; const editorState = this.state.editorState; switch (payload.action) { @@ -868,7 +861,7 @@ export default class MessageComposerInput extends React.Component { return true; } - const newState: ?Value = null; + //const newState: ?Value = null; // Draft handles rich text mode commands by default but we need to do it ourselves for Markdown. if (this.state.isRichTextEnabled) { @@ -1119,7 +1112,9 @@ export default class MessageComposerInput extends React.Component { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Server error', '', ErrorDialog, { title: _t("Server error"), - description: ((err && err.message) ? err.message : _t("Server unavailable, overloaded, or something else went wrong.")), + description: ((err && err.message) ? err.message : _t( + "Server unavailable, overloaded, or something else went wrong.", + )), }); }); } else if (cmd.error) { @@ -1233,7 +1228,7 @@ export default class MessageComposerInput extends React.Component { action: 'message_sent', }); }).catch((e) => { - onSendMessageFailed(e, this.props.room); + this.onSendMessageFailed(e, this.props.room); }); this.setState({ @@ -1498,7 +1493,9 @@ export default class MessageComposerInput extends React.Component { }); const style = {}; if (props.selected) style.border = '1px solid blue'; - return {; + return {; } } }; @@ -1552,7 +1549,6 @@ export default class MessageComposerInput extends React.Component { getSelectionRange(editorState: Value) { let beginning = false; - const query = this.getAutocompleteQuery(editorState); const firstChild = editorState.document.nodes.get(0); const firstGrandChild = firstChild && firstChild.nodes.get(0); beginning = (firstChild && firstGrandChild && From fea19805811006ce54e3ae81c441e046c95d3b68 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 10 Jan 2019 10:23:49 +0000 Subject: [PATCH 72/93] Only 18 warnings now --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8804c0911b..7b55a09948 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "start:init": "babel src -d lib --source-maps --copy-files", "lint": "eslint src/", "lintall": "eslint src/ test/", - "lintwithexclusions": "eslint --max-warnings 19 --ignore-path .eslintignore.errorfiles src test", + "lintwithexclusions": "eslint --max-warnings 18 --ignore-path .eslintignore.errorfiles src test", "clean": "rimraf lib", "prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt", "test": "karma start --single-run=true --browsers ChromeHeadless", From 3752a21fb1ce405065fa10a85d4b4119c17656dd Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 10 Jan 2019 13:49:40 +0000 Subject: [PATCH 73/93] Create set backup niggles: 1 * Clear the password score when hitting 'back' * autoFocus password input --- .../views/dialogs/keybackup/CreateKeyBackupDialog.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index c5a7ff558d..814ba57dab 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -177,6 +177,7 @@ export default React.createClass({ passPhrase: '', passPhraseConfirm: '', phase: PHASE_PASSPHRASE, + zxcvbnResult: null, }); }, @@ -246,6 +247,7 @@ export default React.createClass({ value={this.state.passPhrase} className="mx_CreateKeyBackupDialog_passPhraseInput" placeholder={_t("Enter a passphrase...")} + autoFocus={true} />
{strengthMeter} From 1d209c5064021d00a3c74521b142f4ccc47ae763 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 10 Jan 2019 14:12:43 +0000 Subject: [PATCH 74/93] Set backup niggles: 2 Don't tell the user their pasphrase doesn't match if it's correct so far --- .../keybackup/CreateKeyBackupDialog.js | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index c5a7ff558d..10b587b779 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -294,14 +294,21 @@ export default React.createClass({ _renderPhasePassPhraseConfirm: function() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + let matchText; + if (this.state.passPhraseConfirm === this.state.passPhrase) { + matchText = _t("That matches!"); + } else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) { + // only tell them they're wrong if they've actually gone wrong. + // Security concious readers will note that if you left riot-web unattended + // on this screen, this would make it easy for a malicious person to guess + // your passphrase one letter at a time, but they could get this faster by + // just opening the browser's developer tools and reading it. + // Note that this includes not having typed anything at all. + matchText = _t("That doesn't match."); + } + let passPhraseMatch = null; - if (this.state.passPhraseConfirm.length > 0) { - let matchText; - if (this.state.passPhraseConfirm === this.state.passPhrase) { - matchText = _t("That matches!"); - } else { - matchText = _t("That doesn't match."); - } + if (matchText) { passPhraseMatch =
{matchText}
From e50f15ac158f7e420c79fc04b7ebcb4095b9bba6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 10 Jan 2019 15:17:39 +0000 Subject: [PATCH 75/93] Key backup: Debounce passphrase feedback https://github.com/vector-im/riot-web/issues/8066 --- .../keybackup/CreateKeyBackupDialog.js | 45 ++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index c5a7ff558d..e9512e455a 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -32,6 +32,7 @@ const PHASE_DONE = 5; const PHASE_OPTOUT_CONFIRM = 6; const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. +const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms. // XXX: copied from ShareDialog: factor out into utils function selectText(target) { @@ -63,6 +64,13 @@ export default React.createClass({ componentWillMount: function() { this._recoveryKeyNode = null; this._keyBackupInfo = null; + this._setZxcvbnResultTimeout = null; + }, + + componentWillUnmount: function() { + if (this._setZxcvbnResultTimeout !== null) { + clearTimeout(this._setZxcvbnResultTimeout); + } }, _collectRecoveryKeyNode: function(n) { @@ -150,9 +158,23 @@ export default React.createClass({ this.setState({phase: PHASE_PASSPHRASE_CONFIRM}); }, - _onPassPhraseKeyPress: function(e) { - if (e.key === 'Enter' && this._passPhraseIsValid()) { - this._onPassPhraseNextClick(); + _onPassPhraseKeyPress: async function(e) { + if (e.key === 'Enter') { + // If we're waiting for the timeout before updating the result at this point, + // skip ahead and do it now, otherwise we'll deny the attempt to proceed + // even if the user enetered a valid passphrase + if (this._setZxcvbnResultTimeout !== null) { + clearTimeout(this._setZxcvbnResultTimeout); + this._setZxcvbnResultTimeout = null; + await new Promise((resolve) => { + this.setState({ + zxcvbnResult: scorePassword(this.state.passPhrase), + }, resolve); + }); + } + if (this._passPhraseIsValid()) { + this._onPassPhraseNextClick(); + } } }, @@ -189,11 +211,20 @@ export default React.createClass({ _onPassPhraseChange: function(e) { this.setState({ passPhrase: e.target.value, - // precompute this and keep it in state: zxcvbn is fast but - // we use it in a couple of different places so no point recomputing - // it unnecessarily. - zxcvbnResult: scorePassword(e.target.value), }); + + if (this._setZxcvbnResultTimeout !== null) { + clearTimeout(this._setZxcvbnResultTimeout); + } + this._setZxcvbnResultTimeout = setTimeout(() => { + this._setZxcvbnResultTimeout = null; + this.setState({ + // precompute this and keep it in state: zxcvbn is fast but + // we use it in a couple of different places so no point recomputing + // it unnecessarily. + zxcvbnResult: scorePassword(this.state.passPhrase), + }); + }, PASSPHRASE_FEEDBACK_DELAY); }, _onPassPhraseConfirmChange: function(e) { From bd5e0d182185ae3f3398b3013f25c48e9423162a Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 10 Jan 2019 15:19:56 +0000 Subject: [PATCH 76/93] My favourite typo :( --- .../views/dialogs/keybackup/CreateKeyBackupDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index e9512e455a..a89b8a3509 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -162,7 +162,7 @@ export default React.createClass({ if (e.key === 'Enter') { // If we're waiting for the timeout before updating the result at this point, // skip ahead and do it now, otherwise we'll deny the attempt to proceed - // even if the user enetered a valid passphrase + // even if the user entered a valid passphrase if (this._setZxcvbnResultTimeout !== null) { clearTimeout(this._setZxcvbnResultTimeout); this._setZxcvbnResultTimeout = null; From fa6b33d629752f566d4ffaf2e552a6193c9a1bca Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 10 Jan 2019 16:54:08 +0000 Subject: [PATCH 77/93] Only update component state when feature is enabled --- src/components/views/rooms/AuxPanel.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/AuxPanel.js b/src/components/views/rooms/AuxPanel.js index 48317411c9..5370b4d8b5 100644 --- a/src/components/views/rooms/AuxPanel.js +++ b/src/components/views/rooms/AuxPanel.js @@ -101,7 +101,9 @@ module.exports = React.createClass({ }, _rateLimitedUpdate: new RateLimitedFunc(function() { - this.setState({counters: this._computeCounters()}) + if (SettingsStore.isFeatureEnabled("feature_state_counters")) { + this.setState({counters: this._computeCounters()}); + } }, 500), _computeCounters: function() { From b7252a0db997fc198e4a521c7b9108861ae53136 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 10 Jan 2019 18:13:04 +0000 Subject: [PATCH 78/93] Rephrase comment to make more sense --- .../views/dialogs/keybackup/CreateKeyBackupDialog.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 10b587b779..43c7f4bd69 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -303,7 +303,8 @@ export default React.createClass({ // on this screen, this would make it easy for a malicious person to guess // your passphrase one letter at a time, but they could get this faster by // just opening the browser's developer tools and reading it. - // Note that this includes not having typed anything at all. + // Note that not having typed anything at all will not hit this clause and + // fall through so empty box === no hint. matchText = _t("That doesn't match."); } From 35af3fc6f762d6c061c71621f0059ba7b2c8ddc2 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 10 Jan 2019 15:13:44 -0600 Subject: [PATCH 79/93] Add separate dialog for recovery method removed The "New Recovery Method" dialog would show if either the recovery method had been changed or removed, but the dialog text didn't make much sense for the removed case. This adds a separate dialog customized for the removed case. Fixes https://github.com/vector-im/riot-web/issues/8046. --- res/css/_components.scss | 2 +- ...ialog.scss => _KeyBackupFailedDialog.scss} | 6 +- .../keybackup/NewRecoveryMethodDialog.js | 7 +- .../keybackup/RecoveryMethodRemovedDialog.js | 80 +++++++++++++++++++ src/components/structures/MatrixChat.js | 19 ++++- src/i18n/strings/en_EN.json | 17 ++-- 6 files changed, 114 insertions(+), 17 deletions(-) rename res/css/views/dialogs/keybackup/{_NewRecoveryMethodDialog.scss => _KeyBackupFailedDialog.scss} (87%) create mode 100644 src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 17737aca14..1e2d7ae156 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -51,7 +51,7 @@ @import "./views/dialogs/_ShareDialog.scss"; @import "./views/dialogs/_UnknownDeviceDialog.scss"; @import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss"; -@import "./views/dialogs/keybackup/_NewRecoveryMethodDialog.scss"; +@import "./views/dialogs/keybackup/_KeyBackupFailedDialog.scss"; @import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss"; @import "./views/directory/_NetworkDropdown.scss"; @import "./views/elements/_AccessibleButton.scss"; diff --git a/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss b/res/css/views/dialogs/keybackup/_KeyBackupFailedDialog.scss similarity index 87% rename from res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss rename to res/css/views/dialogs/keybackup/_KeyBackupFailedDialog.scss index f2ebe59925..4a050b6fc4 100644 --- a/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss +++ b/res/css/views/dialogs/keybackup/_KeyBackupFailedDialog.scss @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_NewRecoveryMethodDialog .mx_Dialog_title { +.mx_KeyBackupFailedDialog .mx_Dialog_title { margin-bottom: 32px; } -.mx_NewRecoveryMethodDialog_title { +.mx_KeyBackupFailedDialog_title { position: relative; padding-left: 45px; padding-bottom: 10px; @@ -36,6 +36,6 @@ limitations under the License. } } -.mx_NewRecoveryMethodDialog .mx_Dialog_buttons { +.mx_KeyBackupFailedDialog .mx_Dialog_buttons { margin-top: 36px; } diff --git a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js index c97ce58c07..ad29ba64b2 100644 --- a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js +++ b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js @@ -71,19 +71,20 @@ export default class NewRecoveryMethodDialog extends React.PureComponent { render() { const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); const DialogButtons = sdk.getComponent("views.elements.DialogButtons"); - const title = + + const title = {_t("New Recovery Method")} ; return ( -

{_t( "A new recovery passphrase and key for Secure " + - "Messages has been detected.", + "Messages have been detected.", )}

{_t( "Setting up Secure Messages on this device " + diff --git a/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js b/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js new file mode 100644 index 0000000000..1975fbe6d6 --- /dev/null +++ b/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js @@ -0,0 +1,80 @@ +/* +Copyright 2019 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 PropTypes from "prop-types"; +import sdk from "../../../../index"; +import dis from "../../../../dispatcher"; +import { _t } from "../../../../languageHandler"; +import Modal from "../../../../Modal"; + +export default class RecoveryMethodRemovedDialog extends React.PureComponent { + static propTypes = { + onFinished: PropTypes.func.isRequired, + } + + onGoToSettingsClick = () => { + this.props.onFinished(); + dis.dispatch({ action: 'view_user_settings' }); + } + + onSetupClick = () => { + this.props.onFinished(); + Modal.createTrackedDialogAsync("Key Backup", "Key Backup", + import("./CreateKeyBackupDialog"), + ); + } + + render() { + const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); + const DialogButtons = sdk.getComponent("views.elements.DialogButtons"); + + const title = + {_t("Recovery Method Removed")} + ; + + return ( + +

+

{_t( + "This device has detected that your recovery passphrase and key " + + "for Secure Messages have been removed.", + )}

+

{_t( + "If you did this accidentally, you can setup Secure Messages on " + + "this device which will re-encrypt this device's message " + + "history with a new recovery method.", + )}

+

{_t( + "If you didn't remove the recovery method, an " + + "attacker may be trying to access your account. " + + "Change your account password and set a new recovery " + + "method immediately in Settings.", + )}

+ +
+ + ); + } +} diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 4983e86c49..8480525886 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1447,10 +1447,21 @@ export default React.createClass({ break; } }); - cli.on("crypto.keyBackupFailed", () => { - Modal.createTrackedDialogAsync('New Recovery Method', 'New Recovery Method', - import('../../async-components/views/dialogs/keybackup/NewRecoveryMethodDialog'), - ); + cli.on("crypto.keyBackupFailed", (errcode) => { + switch (errcode) { + case 'M_NOT_FOUND': + Modal.createTrackedDialogAsync('Recovery Method Removed', 'Recovery Method Removed', + import('../../async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog'), + ); + return; + case 'M_WRONG_ROOM_KEYS_VERSION': + Modal.createTrackedDialogAsync('New Recovery Method', 'New Recovery Method', + import('../../async-components/views/dialogs/keybackup/NewRecoveryMethodDialog'), + ); + return; + default: + console.error(`Invalid key backup failure code: ${errcode}`); + } }); // Fire the tinter right on startup to ensure the default theme is applied diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5afac11b75..649081b73e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -261,6 +261,8 @@ "Custom user status messages": "Custom user status messages", "Increase performance by only loading room members on first view": "Increase performance by only loading room members on first view", "Backup of encryption keys to server": "Backup of encryption keys to server", + "Render simple counters in room header": "Render simple counters in room header", + "Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu": "Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu", "Disable Emoji suggestions while typing": "Disable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", "Hide removed messages": "Hide removed messages", @@ -537,6 +539,7 @@ "Forget room": "Forget room", "Search": "Search", "Share room": "Share room", + "Toggle right panel": "Toggle right panel", "Drop here to favourite": "Drop here to favourite", "Drop here to tag direct chat": "Drop here to tag direct chat", "Drop here to restore": "Drop here to restore", @@ -1079,6 +1082,7 @@ "Direct Chat": "Direct Chat", "Set a new status...": "Set a new status...", "Clear status": "Clear status", + "View as Grid": "View as Grid", "View Community": "View Community", "Sorry, your browser is not able to run Riot.": "Sorry, your browser is not able to run Riot.", "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.", @@ -1089,6 +1093,7 @@ "You must register to use this functionality": "You must register to use this functionality", "You must join the room to see its files": "You must join the room to see its files", "There are no visible files in this room": "There are no visible files in this room", + "No room in this tile yet.": "No room in this tile yet.", "

HTML for your community's page

\n

\n Use the long description to introduce new members to the community, or distribute\n some important links\n

\n

\n You can even use 'img' tags\n

\n": "

HTML for your community's page

\n

\n Use the long description to introduce new members to the community, or distribute\n some important links\n

\n

\n You can even use 'img' tags\n

\n", "Add rooms to the community summary": "Add rooms to the community summary", "Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?", @@ -1403,16 +1408,16 @@ "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.", "If you don't want to set this up now, you can later in Settings.": "If you don't want to set this up now, you can later in Settings.", "New Recovery Method": "New Recovery Method", - "A new recovery passphrase and key for Secure Messages has been detected.": "A new recovery passphrase and key for Secure Messages has been detected.", + "A new recovery passphrase and key for Secure Messages have been detected.": "A new recovery passphrase and key for Secure Messages have been detected.", "Setting up Secure Messages on this device will re-encrypt this device's message history with the new recovery method.": "Setting up Secure Messages on this device will re-encrypt this device's message history with the new recovery method.", "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.", "Set up Secure Messages": "Set up Secure Messages", "Go to Settings": "Go to Settings", + "Recovery Method Removed": "Recovery Method Removed", + "This device has detected that your recovery passphrase and key for Secure Messages have been removed.": "This device has detected that your recovery passphrase and key for Secure Messages have been removed.", + "If you did this accidentally, you can setup Secure Messages on this device which will re-encrypt this device's message history with a new recovery method.": "If you did this accidentally, you can setup Secure Messages on this device which will re-encrypt this device's message history with a new recovery method.", + "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", - "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room", - "Render simple counters in room header": "Render simple counters in room header", - "View as Grid": "View as Grid", - "Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu": "Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu", - "No room in this tile yet.": "No room in this tile yet." + "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" } From cec9c908fcf01cbd6b0f2bf914fbeeea7d5aec10 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 10 Jan 2019 14:33:46 -0700 Subject: [PATCH 80/93] Set which servers to try and join upgraded rooms through Fixes https://github.com/vector-im/riot-web/issues/7991 --- src/components/views/rooms/MessageComposer.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index e15ca047ac..aefee9809a 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -285,6 +285,10 @@ export default class MessageComposer extends React.Component { action: 'view_room', highlighted: true, room_id: replacementRoomId, + + // Try to join via the server that sent the event. This converts $something:example.org + // into a server domain by splitting on colons and ignoring the first entry ("$something"). + via_servers: [this.state.tombstone.getId().split(':').splice(1).join(':')], }); } From 18ba5f6f19b9b81894a7db823a760be3c2523d3d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 10 Jan 2019 14:44:19 -0700 Subject: [PATCH 81/93] Don't show rooms with tombstones in the address picker Fixes https://github.com/vector-im/riot-web/issues/8076 --- src/components/views/dialogs/AddressPickerDialog.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index cbe80763a6..6276a45839 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -389,6 +389,17 @@ module.exports = React.createClass({ const suggestedList = []; results.forEach((result) => { if (result.room_id) { + const client = MatrixClientPeg.get(); + const room = client.getRoom(result.room_id); + if (room) { + const tombstone = room.currentState.getStateEvents('m.room.tombstone', ''); + if (tombstone && tombstone.getContent() && tombstone.getContent()["replacement_room"]) { + const replacementRoom = client.getRoom(tombstone.getContent()["replacement_room"]); + + // Skip rooms with tombstones where we are also aware of the replacement room. + if (replacementRoom) return; + } + } suggestedList.push({ addressType: 'mx-room-id', address: result.room_id, From e24d3cd67105f5d5b1e2f1b6750a79ccc1acce5a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 10 Jan 2019 15:15:45 -0700 Subject: [PATCH 82/93] Render a tile for tombstone events Fixes https://github.com/vector-im/riot-web/issues/7997 This isn't super elegant, but it also provides some amount of utility for people. As users might leave the old room, it might be useful to see when exactly a room was upgraded. We should fix the underlying cause for infinite back pagination though. --- src/TextForEvent.js | 6 ++++++ src/components/views/rooms/EventTile.js | 1 + src/i18n/strings/en_EN.json | 1 + 3 files changed, 8 insertions(+) diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 96cccf07fb..2a37295f83 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -129,6 +129,11 @@ function textForRoomNameEvent(ev) { }); } +function textForTombstoneEvent(ev) { + const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); + return _t('%(senderDisplayName)s upgraded this room.', {senderDisplayName}); +} + function textForServerACLEvent(ev) { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const prevContent = ev.getPrevContent(); @@ -433,6 +438,7 @@ const stateHandlers = { 'm.room.power_levels': textForPowerEvent, 'm.room.pinned_events': textForPinnedEvent, 'm.room.server_acl': textForServerACLEvent, + 'm.room.tombstone': textForTombstoneEvent, 'im.vector.modular.widgets': textForWidgetEvent, }; diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index e978bf438a..692111361a 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -62,6 +62,7 @@ const stateEventTileTypes = { 'm.room.pinned_events': 'messages.TextualEvent', 'm.room.server_acl': 'messages.TextualEvent', 'im.vector.modular.widgets': 'messages.TextualEvent', + 'm.room.tombstone': 'messages.TextualEvent', }; function getHandlerTile(ev) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5afac11b75..0faaa5d99f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -183,6 +183,7 @@ "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s changed the topic to \"%(topic)s\".", "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s removed the room name.", "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s changed the room name to %(roomName)s.", + "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s upgraded this room.", "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.", "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s added %(addedAddresses)s as addresses for this room.", "%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s added %(addedAddresses)s as an address for this room.", From a5fceefc63bc2aabf7ab059c7c27347861a5bed3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 10 Jan 2019 15:17:20 -0700 Subject: [PATCH 83/93] Regenerate en_EN.json to sort entries --- src/i18n/strings/en_EN.json | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5afac11b75..1a1a3743ec 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -261,6 +261,8 @@ "Custom user status messages": "Custom user status messages", "Increase performance by only loading room members on first view": "Increase performance by only loading room members on first view", "Backup of encryption keys to server": "Backup of encryption keys to server", + "Render simple counters in room header": "Render simple counters in room header", + "Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu": "Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu", "Disable Emoji suggestions while typing": "Disable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", "Hide removed messages": "Hide removed messages", @@ -537,6 +539,7 @@ "Forget room": "Forget room", "Search": "Search", "Share room": "Share room", + "Toggle right panel": "Toggle right panel", "Drop here to favourite": "Drop here to favourite", "Drop here to tag direct chat": "Drop here to tag direct chat", "Drop here to restore": "Drop here to restore", @@ -1079,6 +1082,7 @@ "Direct Chat": "Direct Chat", "Set a new status...": "Set a new status...", "Clear status": "Clear status", + "View as Grid": "View as Grid", "View Community": "View Community", "Sorry, your browser is not able to run Riot.": "Sorry, your browser is not able to run Riot.", "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.", @@ -1089,6 +1093,7 @@ "You must register to use this functionality": "You must register to use this functionality", "You must join the room to see its files": "You must join the room to see its files", "There are no visible files in this room": "There are no visible files in this room", + "No room in this tile yet.": "No room in this tile yet.", "

HTML for your community's page

\n

\n Use the long description to introduce new members to the community, or distribute\n some important links\n

\n

\n You can even use 'img' tags\n

\n": "

HTML for your community's page

\n

\n Use the long description to introduce new members to the community, or distribute\n some important links\n

\n

\n You can even use 'img' tags\n

\n", "Add rooms to the community summary": "Add rooms to the community summary", "Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?", @@ -1410,9 +1415,5 @@ "Go to Settings": "Go to Settings", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", - "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room", - "Render simple counters in room header": "Render simple counters in room header", - "View as Grid": "View as Grid", - "Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu": "Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu", - "No room in this tile yet.": "No room in this tile yet." + "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" } From b09219fe33ba6af94b6df4b5ed0b441cefa2c852 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 10 Jan 2019 15:29:12 -0700 Subject: [PATCH 84/93] Navigate to the upgraded room's create event where possible Fixes https://github.com/vector-im/riot-web/issues/7998 --- src/components/views/rooms/MessageComposer.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index e15ca047ac..ddd3058a5b 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -281,9 +281,17 @@ export default class MessageComposer extends React.Component { ev.preventDefault(); const replacementRoomId = this.state.tombstone.getContent()['replacement_room']; + const replacementRoom = MatrixClientPeg.get().getRoom(replacementRoomId); + let createEventId = null; + if (replacementRoom) { + const createEvent = replacementRoom.currentState.getStateEvents('m.room.create', ''); + if (createEvent && createEvent.getId()) createEventId = createEvent.getId(); + } + this.props.roomViewStore.getDispatcher().dispatch({ action: 'view_room', highlighted: true, + event_id: createEventId, room_id: replacementRoomId, }); } From 7d4b6add2cfd3d71dd093b55be90248414050b7a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 10 Jan 2019 15:43:22 -0700 Subject: [PATCH 85/93] Recalculate the visible rooms when rooms are upgraded We also do this when they are created just in case we missed the tombstone event. Fixes https://github.com/vector-im/riot-web/issues/7992 --- src/components/views/rooms/RoomList.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index dbfe95dadf..ed8142cd53 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -102,6 +102,7 @@ module.exports = React.createClass({ cli.on("Event.decrypted", this.onEventDecrypted); cli.on("accountData", this.onAccountData); cli.on("Group.myMembership", this._onGroupMyMembership); + cli.on("RoomState.events", this.onRoomStateEvents); const dmRoomMap = DMRoomMap.shared(); // A map between tags which are group IDs and the room IDs of rooms that should be kept @@ -226,6 +227,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().removeListener("Event.decrypted", this.onEventDecrypted); MatrixClientPeg.get().removeListener("accountData", this.onAccountData); MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership); + MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); } if (this._tagStoreToken) { @@ -249,6 +251,12 @@ module.exports = React.createClass({ this.updateVisibleRooms(); }, + onRoomStateEvents: function(ev, state) { + if (ev.getType() === "m.room.create" || ev.getType() === "m.room.tombstone") { + this.updateVisibleRooms(); + } + }, + onDeleteRoom: function(roomId) { this.updateVisibleRooms(); }, From 5333114d7bd6e83f813b71be9a55bb883976dc8c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 10 Jan 2019 21:43:21 -0700 Subject: [PATCH 86/93] Give a route for retrying invites for users which may not exist Fixes https://github.com/vector-im/riot-web/issues/7922 This supports the current style of errors (M_NOT_FOUND) as well as the errors presented by MSC1797: https://github.com/matrix-org/matrix-doc/pull/1797 --- src/components/structures/UserSettings.js | 1 + .../views/dialogs/RetryInvitesDialog.js | 78 ++++++++ src/i18n/strings/en_EN.json | 6 + src/settings/Settings.js | 5 + src/utils/MultiInviter.js | 168 ++++++++++++------ 5 files changed, 207 insertions(+), 51 deletions(-) create mode 100644 src/components/views/dialogs/RetryInvitesDialog.js diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index b9dbe345c5..6ba7bcc4dc 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -86,6 +86,7 @@ const SIMPLE_SETTINGS = [ { id: "pinMentionedRooms" }, { id: "pinUnreadRooms" }, { id: "showDeveloperTools" }, + { id: "alwaysRetryInvites" }, ]; // These settings must be defined in SettingsStore diff --git a/src/components/views/dialogs/RetryInvitesDialog.js b/src/components/views/dialogs/RetryInvitesDialog.js new file mode 100644 index 0000000000..24647ae4a0 --- /dev/null +++ b/src/components/views/dialogs/RetryInvitesDialog.js @@ -0,0 +1,78 @@ +/* +Copyright 2019 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 PropTypes from 'prop-types'; +import sdk from '../../../index'; +import { _t } from '../../../languageHandler'; +import {SettingLevel} from "../../../settings/SettingsStore"; +import SettingsStore from "../../../settings/SettingsStore"; + +export default React.createClass({ + propTypes: { + failedInvites: PropTypes.object.isRequired, // { address: { errcode, errorText } } + onTryAgain: PropTypes.func.isRequired, + onGiveUp: PropTypes.func.isRequired, + onFinished: PropTypes.func.isRequired, + }, + + _onTryAgainClicked: function() { + this.props.onTryAgain(); + this.props.onFinished(true); + }, + + _onTryAgainNeverWarnClicked: function() { + SettingsStore.setValue("alwaysRetryInvites", null, SettingLevel.ACCOUNT, true); + this.props.onTryAgain(); + this.props.onFinished(true); + }, + + _onGiveUpClicked: function() { + this.props.onGiveUp(); + this.props.onFinished(false); + }, + + render: function() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + + const errorList = Object.keys(this.props.failedInvites) + .map(address =>

{address}: {this.props.failedInvites[address].errorText}

); + + return ( + +
+ { errorList } +
+ +
+ + + +
+
+ ); + }, +}); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ef659bf566..816506f6c3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -222,8 +222,10 @@ "Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions", "Not a valid Riot keyfile": "Not a valid Riot keyfile", "Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?", + "Unrecognised address": "Unrecognised address", "You do not have permission to invite people to this room.": "You do not have permission to invite people to this room.", "User %(user_id)s does not exist": "User %(user_id)s does not exist", + "User %(user_id)s may or may not exist": "User %(user_id)s may or may not exist", "Unknown server error": "Unknown server error", "Use a few words, avoid common phrases": "Use a few words, avoid common phrases", "No need for symbols, digits, or uppercase letters": "No need for symbols, digits, or uppercase letters", @@ -291,6 +293,7 @@ "Pin unread rooms to the top of the room list": "Pin unread rooms to the top of the room list", "Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets", "Show empty room list headings": "Show empty room list headings", + "Always retry invites for unknown users": "Always retry invites for unknown users", "Show developer tools": "Show developer tools", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", @@ -965,6 +968,9 @@ "Clear cache and resync": "Clear cache and resync", "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!", "Updating Riot": "Updating Riot", + "Failed to invite the following users": "Failed to invite the following users", + "Try again and never warn me again": "Try again and never warn me again", + "Try again": "Try again", "Failed to upgrade room": "Failed to upgrade room", "The room upgrade could not be completed": "The room upgrade could not be completed", "Upgrade this room to version %(version)s": "Upgrade this room to version %(version)s", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 1cac8559d1..507bcf49b8 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -317,6 +317,11 @@ export const SETTINGS = { displayName: _td('Show empty room list headings'), default: true, }, + "alwaysRetryInvites": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td('Always retry invites for unknown users'), + default: false, + }, "showDeveloperTools": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Show developer tools'), diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js index ad10f28edf..0d7a8837b8 100644 --- a/src/utils/MultiInviter.js +++ b/src/utils/MultiInviter.js @@ -15,11 +15,15 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from "react"; import MatrixClientPeg from '../MatrixClientPeg'; import {getAddressType} from '../UserAddress'; import GroupStore from '../stores/GroupStore'; import Promise from 'bluebird'; import {_t} from "../languageHandler"; +import sdk from "../index"; +import Modal from "../Modal"; +import SettingsStore from "../settings/SettingsStore"; /** * Invites multiple addresses to a room or group, handling rate limiting from the server @@ -41,7 +45,7 @@ export default class MultiInviter { this.addrs = []; this.busy = false; this.completionStates = {}; // State of each address (invited or error) - this.errorTexts = {}; // Textual error per address + this.errors = {}; // { address: {errorText, errcode} } this.deferred = null; } @@ -61,7 +65,10 @@ export default class MultiInviter { for (const addr of this.addrs) { if (getAddressType(addr) === null) { this.completionStates[addr] = 'error'; - this.errorTexts[addr] = 'Unrecognised address'; + this.errors[addr] = { + errcode: 'M_INVALID', + errorText: _t('Unrecognised address'), + }; } } this.deferred = Promise.defer(); @@ -85,18 +92,23 @@ export default class MultiInviter { } getErrorText(addr) { - return this.errorTexts[addr]; + return this.errors[addr] ? this.errors[addr].errorText : null; } - async _inviteToRoom(roomId, addr) { + async _inviteToRoom(roomId, addr, ignoreProfile) { const addrType = getAddressType(addr); if (addrType === 'email') { return MatrixClientPeg.get().inviteByEmail(roomId, addr); } else if (addrType === 'mx-user-id') { - const profile = await MatrixClientPeg.get().getProfileInfo(addr); - if (!profile) { - return Promise.reject({errcode: "M_NOT_FOUND", error: "User does not have a profile."}); + if (!ignoreProfile && !SettingsStore.getValue("alwaysRetryInvites", this.roomId)) { + const profile = await MatrixClientPeg.get().getProfileInfo(addr); + if (!profile) { + return Promise.reject({ + errcode: "M_NOT_FOUND", + error: "User does not have a profile or does not exist.", + }); + } } return MatrixClientPeg.get().invite(roomId, addr); @@ -105,19 +117,113 @@ export default class MultiInviter { } } + _doInvite(address, ignoreProfile) { + return new Promise((resolve, reject) => { + let doInvite; + if (this.groupId !== null) { + doInvite = GroupStore.inviteUserToGroup(this.groupId, address); + } else { + doInvite = this._inviteToRoom(this.roomId, address, ignoreProfile); + } - _inviteMore(nextIndex) { + doInvite.then(() => { + if (this._canceled) { + return; + } + + this.completionStates[address] = 'invited'; + delete this.errors[address]; + + resolve(); + }).catch((err) => { + if (this._canceled) { + return; + } + + let errorText; + let fatal = false; + if (err.errcode === 'M_FORBIDDEN') { + fatal = true; + errorText = _t('You do not have permission to invite people to this room.'); + } else if (err.errcode === 'M_LIMIT_EXCEEDED') { + // we're being throttled so wait a bit & try again + setTimeout(() => { + this._doInvite(address, ignoreProfile).then(resolve, reject); + }, 5000); + return; + } else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND'].includes(err.errcode)) { + errorText = _t("User %(user_id)s does not exist", {user_id: address}); + } else if (err.errcode === 'M_PROFILE_UNKNOWN') { + errorText = _t("User %(user_id)s may or may not exist", {user_id: address}); + } else if (err.errcode === 'M_PROFILE_NOT_FOUND' && !ignoreProfile) { + // Invite without the profile check + console.warn(`User ${address} does not have a profile - trying invite again`); + this._doInvite(address, true).then(resolve, reject); + } else { + errorText = _t('Unknown server error'); + } + + this.completionStates[address] = 'error'; + this.errors[address] = {errorText, errcode: err.errcode}; + + this.busy = !fatal; + this.fatal = fatal; + + if (fatal) { + reject(); + } else { + resolve(); + } + }); + }); + } + + _inviteMore(nextIndex, ignoreProfile) { if (this._canceled) { return; } if (nextIndex === this.addrs.length) { this.busy = false; + if (Object.keys(this.errors).length > 0 && !this.groupId) { + // There were problems inviting some people - see if we can invite them + // without caring if they exist or not. + const reinviteErrors = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNKNOWN', 'M_PROFILE_NOT_FOUND']; + const reinvitableUsers = Object.keys(this.errors).filter(a => reinviteErrors.includes(this.errors[a].errcode)); + + if (reinvitableUsers.length > 0) { + const retryInvites = () => { + const promises = reinvitableUsers.map(u => this._doInvite(u, true)); + Promise.all(promises).then(() => this.deferred.resolve(this.completionStates)); + }; + + if (SettingsStore.getValue("alwaysRetryInvites", this.roomId)) { + retryInvites(); + return; + } + + const RetryInvitesDialog = sdk.getComponent("dialogs.RetryInvitesDialog"); + console.log("Showing failed to invite dialog..."); + Modal.createTrackedDialog('Failed to invite the following users to the room', '', RetryInvitesDialog, { + failedInvites: this.errors, + onTryAgain: () => retryInvites(), + onGiveUp: () => { + // Fake all the completion states because we already warned the user + for (const addr of Object.keys(this.completionStates)) { + this.completionStates[addr] = 'invited'; + } + this.deferred.resolve(this.completionStates); + }, + }); + return; + } + } this.deferred.resolve(this.completionStates); return; } const addr = this.addrs[nextIndex]; + console.log(`Inviting ${addr}`); // don't try to invite it if it's an invalid address // (it will already be marked as an error though, @@ -134,48 +240,8 @@ export default class MultiInviter { return; } - let doInvite; - if (this.groupId !== null) { - doInvite = GroupStore.inviteUserToGroup(this.groupId, addr); - } else { - doInvite = this._inviteToRoom(this.roomId, addr); - } - - doInvite.then(() => { - if (this._canceled) { return; } - - this.completionStates[addr] = 'invited'; - - this._inviteMore(nextIndex + 1); - }).catch((err) => { - if (this._canceled) { return; } - - let errorText; - let fatal = false; - if (err.errcode === 'M_FORBIDDEN') { - fatal = true; - errorText = _t('You do not have permission to invite people to this room.'); - } else if (err.errcode === 'M_LIMIT_EXCEEDED') { - // we're being throttled so wait a bit & try again - setTimeout(() => { - this._inviteMore(nextIndex); - }, 5000); - return; - } else if(err.errcode === "M_NOT_FOUND") { - errorText = _t("User %(user_id)s does not exist", {user_id: addr}); - } else { - errorText = _t('Unknown server error'); - } - this.completionStates[addr] = 'error'; - this.errorTexts[addr] = errorText; - this.busy = !fatal; - this.fatal = fatal; - - if (!fatal) { - this._inviteMore(nextIndex + 1); - } else { - this.deferred.resolve(this.completionStates); - } - }); + this._doInvite(addr, ignoreProfile).then(() => { + this._inviteMore(nextIndex + 1, ignoreProfile); + }).catch(() => this.deferred.resolve(this.completionStates)); } } From c351ee3d30d2fe6ae3fa8a023fa783791ee98162 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 10 Jan 2019 21:54:07 -0700 Subject: [PATCH 87/93] Appease the linter --- src/components/views/dialogs/RetryInvitesDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/RetryInvitesDialog.js b/src/components/views/dialogs/RetryInvitesDialog.js index 24647ae4a0..f27b0bc08b 100644 --- a/src/components/views/dialogs/RetryInvitesDialog.js +++ b/src/components/views/dialogs/RetryInvitesDialog.js @@ -49,7 +49,7 @@ export default React.createClass({ const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const errorList = Object.keys(this.props.failedInvites) - .map(address =>

{address}: {this.props.failedInvites[address].errorText}

); + .map(address =>

{address}: {this.props.failedInvites[address].errorText}

); return ( Date: Fri, 11 Jan 2019 13:15:09 +0000 Subject: [PATCH 88/93] Different dialog for new trusted backup Split the 'new recovery method' into two cases: one where the new recovery method isn't trusted and you need to verify the device, and another where it is and the client is using it (where it's more of an FYI). https://github.com/vector-im/riot-web/issues/8069 --- .../keybackup/NewRecoveryMethodDialog.js | 80 +++++++++++++------ src/components/structures/MatrixChat.js | 40 ++++++---- src/i18n/strings/en_EN.json | 5 +- 3 files changed, 83 insertions(+), 42 deletions(-) diff --git a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js index ad29ba64b2..bd191a7c13 100644 --- a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js +++ b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js @@ -1,5 +1,5 @@ /* -Copyright 2018 New Vector Ltd +Copyright 2018-2019 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. @@ -24,9 +24,15 @@ import Modal from "../../../../Modal"; export default class NewRecoveryMethodDialog extends React.PureComponent { static propTypes = { + // As returned by js-sdk getKeyBackupVersion() + newVersionInfo: PropTypes.object.isRequired, onFinished: PropTypes.func.isRequired, } + onOkClick = () => { + this.props.onFinished(); + } + onGoToSettingsClick = () => { this.props.onFinished(); dis.dispatch({ action: 'view_user_settings' }); @@ -41,8 +47,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent { // sending our own new keys to it. let backupSigStatus; try { - const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); - backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo); + backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(this.props.newVersionInfo); } catch (e) { console.log("Unable to fetch key backup status", e); return; @@ -76,34 +81,57 @@ export default class NewRecoveryMethodDialog extends React.PureComponent { {_t("New Recovery Method")} ; + const newMethodDetected =

{_t( + "A new recovery passphrase and key for Secure " + + "Messages have been detected.", + )}

; + + const hackWarning =

{_t( + "If you didn't set the new recovery method, an " + + "attacker may be trying to access your account. " + + "Change your account password and set a new recovery " + + "method immediately in Settings.", + )}

; + + let content; + if (MatrixClientPeg.get().getKeyBackupEnabled()) { + content =
+ {newMethodDetected} +

{_t( + "This device is encrypting history using the new recovery method." + )}

+ {hackWarning} + +
; + } else { + content =
+ {newMethodDetected} +

{_t( + "Setting up Secure Messages on this device " + + "will re-encrypt this device's message history with " + + "the new recovery method.", + )}

+ {hackWarning} + +
; + } + return ( -
-

{_t( - "A new recovery passphrase and key for Secure " + - "Messages have been detected.", - )}

-

{_t( - "Setting up Secure Messages on this device " + - "will re-encrypt this device's message history with " + - "the new recovery method.", - )}

-

{_t( - "If you didn't set the new recovery method, an " + - "attacker may be trying to access your account. " + - "Change your account password and set a new recovery " + - "method immediately in Settings.", - )}

- -
+ {content}
); } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 8480525886..a1913f13be 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1,7 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd -Copyright 2017, 2018 New Vector Ltd +Copyright 2017-2019 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. @@ -1447,20 +1447,32 @@ export default React.createClass({ break; } }); - cli.on("crypto.keyBackupFailed", (errcode) => { - switch (errcode) { - case 'M_NOT_FOUND': - Modal.createTrackedDialogAsync('Recovery Method Removed', 'Recovery Method Removed', - import('../../async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog'), - ); + cli.on("crypto.keyBackupFailed", async (errcode) => { + let haveNewVersion; + let newVersionInfo; + // if key backup is still enabled, there must be a new backup in place + if (MatrixClientPeg.get().getKeyBackupEnabled()) { + haveNewVersion = true; + } else { + // otherwise check the server to see if there's a new one + try { + newVersionInfo = await MatrixClientPeg.get().getKeyBackupVersion(); + if (newVersionInfo !== null) haveNewVersion = true; + } catch (e) { + console.error("Saw key backup error but failed to check backup version!", e); return; - case 'M_WRONG_ROOM_KEYS_VERSION': - Modal.createTrackedDialogAsync('New Recovery Method', 'New Recovery Method', - import('../../async-components/views/dialogs/keybackup/NewRecoveryMethodDialog'), - ); - return; - default: - console.error(`Invalid key backup failure code: ${errcode}`); + } + } + + if (haveNewVersion) { + Modal.createTrackedDialogAsync('New Recovery Method', 'New Recovery Method', + import('../../async-components/views/dialogs/keybackup/NewRecoveryMethodDialog'), + { newVersionInfo } + ); + } else { + Modal.createTrackedDialogAsync('Recovery Method Removed', 'Recovery Method Removed', + import('../../async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog'), + ); } }); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9e5b98afdd..0fbed11e20 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1410,10 +1410,11 @@ "If you don't want to set this up now, you can later in Settings.": "If you don't want to set this up now, you can later in Settings.", "New Recovery Method": "New Recovery Method", "A new recovery passphrase and key for Secure Messages have been detected.": "A new recovery passphrase and key for Secure Messages have been detected.", - "Setting up Secure Messages on this device will re-encrypt this device's message history with the new recovery method.": "Setting up Secure Messages on this device will re-encrypt this device's message history with the new recovery method.", "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.", - "Set up Secure Messages": "Set up Secure Messages", + "This device is encrypting history using the new recovery method.": "This device is encrypting history using the new recovery method.", "Go to Settings": "Go to Settings", + "Setting up Secure Messages on this device will re-encrypt this device's message history with the new recovery method.": "Setting up Secure Messages on this device will re-encrypt this device's message history with the new recovery method.", + "Set up Secure Messages": "Set up Secure Messages", "Recovery Method Removed": "Recovery Method Removed", "This device has detected that your recovery passphrase and key for Secure Messages have been removed.": "This device has detected that your recovery passphrase and key for Secure Messages have been removed.", "If you did this accidentally, you can setup Secure Messages on this device which will re-encrypt this device's message history with a new recovery method.": "If you did this accidentally, you can setup Secure Messages on this device which will re-encrypt this device's message history with a new recovery method.", From 5c9d41d96bcfca1f5c2529579490064b132390f4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 11 Jan 2019 13:42:40 +0000 Subject: [PATCH 89/93] Lint --- .../views/dialogs/keybackup/NewRecoveryMethodDialog.js | 2 +- src/components/structures/MatrixChat.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js index bd191a7c13..15c410c93c 100644 --- a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js +++ b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js @@ -98,7 +98,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent { content =
{newMethodDetected}

{_t( - "This device is encrypting history using the new recovery method." + "This device is encrypting history using the new recovery method.", )}

{hackWarning} Date: Fri, 11 Jan 2019 13:54:11 +0000 Subject: [PATCH 90/93] De-lint a few more files & remove them from the ignored list --- .eslintignore.errorfiles | 3 --- src/autocomplete/AutocompleteProvider.js | 6 +++++- src/autocomplete/Autocompleter.js | 4 ++-- src/autocomplete/UserProvider.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index 1c9e2d4413..2b57d4e9e2 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -1,8 +1,5 @@ # autogenerated file: run scripts/generate-eslint-error-ignore-file to update. -src/autocomplete/AutocompleteProvider.js -src/autocomplete/Autocompleter.js -src/autocomplete/UserProvider.js src/component-index.js src/components/structures/BottomLeftMenu.js src/components/structures/CompatibilityPage.js diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index f9fb61d3a3..906fed5858 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.js @@ -41,8 +41,12 @@ export default class AutocompleteProvider { /** * Of the matched commands in the query, returns the first that contains or is contained by the selection, or null. + * @param {string} query The query string + * @param {SelectionRange} selection Selection to search + * @param {boolean} force True if the user is forcing completion + * @return {object} { command, range } where both onjects fields are null if no match */ - getCurrentCommand(query: string, selection: SelectionRange, force: boolean = false): ?string { + getCurrentCommand(query: string, selection: SelectionRange, force: boolean = false) { let commandRegex = this.commandRegex; if (force && this.shouldForceComplete()) { diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index e7b89fe576..af2744950f 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -60,8 +60,8 @@ const PROVIDER_COMPLETION_TIMEOUT = 3000; export default class Autocompleter { constructor(room: Room) { this.room = room; - this.providers = PROVIDERS.map((p) => { - return new p(room); + this.providers = PROVIDERS.map((Prov) => { + return new Prov(room); }); } diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 2eae053d72..d4a5ec5e74 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -41,7 +41,7 @@ export default class UserProvider extends AutocompleteProvider { users: Array = null; room: Room = null; - constructor(room) { + constructor(room: Room) { super(USER_REGEX, FORCED_USER_REGEX); this.room = room; this.matcher = new QueryMatcher([], { From 77efa0881e9aec3ceff09a50290dd6170a499e40 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 11 Jan 2019 14:09:29 +0000 Subject: [PATCH 91/93] Gah, onjects --- src/autocomplete/AutocompleteProvider.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index 906fed5858..98ae83c526 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.js @@ -44,7 +44,7 @@ export default class AutocompleteProvider { * @param {string} query The query string * @param {SelectionRange} selection Selection to search * @param {boolean} force True if the user is forcing completion - * @return {object} { command, range } where both onjects fields are null if no match + * @return {object} { command, range } where both objects fields are null if no match */ getCurrentCommand(query: string, selection: SelectionRange, force: boolean = false) { let commandRegex = this.commandRegex; From c650ace0c042c38686c798cbd30b65fc29dd11d3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 11 Jan 2019 18:39:16 +0000 Subject: [PATCH 92/93] This shouldn't be required --- .../views/dialogs/keybackup/NewRecoveryMethodDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js index 15c410c93c..db86178b5a 100644 --- a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js +++ b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js @@ -25,7 +25,7 @@ import Modal from "../../../../Modal"; export default class NewRecoveryMethodDialog extends React.PureComponent { static propTypes = { // As returned by js-sdk getKeyBackupVersion() - newVersionInfo: PropTypes.object.isRequired, + newVersionInfo: PropTypes.object, onFinished: PropTypes.func.isRequired, } From a05c0f9214833e119f09a0f7742a156482dd88f0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 11 Jan 2019 15:46:03 -0700 Subject: [PATCH 93/93] Rephrase everything to be "invite anyways" rather than "retry" Also handle profile errors better --- ...itesDialog.js => AskInviteAnywayDialog.js} | 33 ++++++------ src/i18n/strings/en_EN.json | 9 ++-- src/settings/Settings.js | 4 +- src/utils/MultiInviter.js | 52 +++++++++++-------- 4 files changed, 54 insertions(+), 44 deletions(-) rename src/components/views/dialogs/{RetryInvitesDialog.js => AskInviteAnywayDialog.js} (63%) diff --git a/src/components/views/dialogs/RetryInvitesDialog.js b/src/components/views/dialogs/AskInviteAnywayDialog.js similarity index 63% rename from src/components/views/dialogs/RetryInvitesDialog.js rename to src/components/views/dialogs/AskInviteAnywayDialog.js index f27b0bc08b..5c61c3a694 100644 --- a/src/components/views/dialogs/RetryInvitesDialog.js +++ b/src/components/views/dialogs/AskInviteAnywayDialog.js @@ -23,20 +23,20 @@ import SettingsStore from "../../../settings/SettingsStore"; export default React.createClass({ propTypes: { - failedInvites: PropTypes.object.isRequired, // { address: { errcode, errorText } } - onTryAgain: PropTypes.func.isRequired, + unknownProfileUsers: PropTypes.array.isRequired, // [ {userId, errorText}... ] + onInviteAnyways: PropTypes.func.isRequired, onGiveUp: PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired, }, - _onTryAgainClicked: function() { - this.props.onTryAgain(); + _onInviteClicked: function() { + this.props.onInviteAnyways(); this.props.onFinished(true); }, - _onTryAgainNeverWarnClicked: function() { - SettingsStore.setValue("alwaysRetryInvites", null, SettingLevel.ACCOUNT, true); - this.props.onTryAgain(); + _onInviteNeverWarnClicked: function() { + SettingsStore.setValue("alwaysInviteUnknownUsers", null, SettingLevel.ACCOUNT, true); + this.props.onInviteAnyways(); this.props.onFinished(true); }, @@ -48,28 +48,31 @@ export default React.createClass({ render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const errorList = Object.keys(this.props.failedInvites) - .map(address =>

{address}: {this.props.failedInvites[address].errorText}

); + const errorList = this.props.unknownProfileUsers + .map(address =>
  • {address.userId}: {address.errorText}
  • ); return (
    - { errorList } +

    {_t("The following users may not exist - would you like to invite them anyways?")}

    +
      + { errorList } +
    - -
    diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 816506f6c3..4f8674db2f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -293,7 +293,7 @@ "Pin unread rooms to the top of the room list": "Pin unread rooms to the top of the room list", "Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets", "Show empty room list headings": "Show empty room list headings", - "Always retry invites for unknown users": "Always retry invites for unknown users", + "Always invite users which may not exist": "Always invite users which may not exist", "Show developer tools": "Show developer tools", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", @@ -884,6 +884,10 @@ "That doesn't look like a valid email address": "That doesn't look like a valid email address", "You have entered an invalid address.": "You have entered an invalid address.", "Try using one of the following valid address types: %(validTypesList)s.": "Try using one of the following valid address types: %(validTypesList)s.", + "The following users may not exist": "The following users may not exist", + "The following users may not exist - would you like to invite them anyways?": "The following users may not exist - would you like to invite them anyways?", + "Invite anyways and never warn me again": "Invite anyways and never warn me again", + "Invite anyways": "Invite anyways", "Preparing to send logs": "Preparing to send logs", "Logs sent": "Logs sent", "Thank you!": "Thank you!", @@ -968,9 +972,6 @@ "Clear cache and resync": "Clear cache and resync", "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!", "Updating Riot": "Updating Riot", - "Failed to invite the following users": "Failed to invite the following users", - "Try again and never warn me again": "Try again and never warn me again", - "Try again": "Try again", "Failed to upgrade room": "Failed to upgrade room", "The room upgrade could not be completed": "The room upgrade could not be completed", "Upgrade this room to version %(version)s": "Upgrade this room to version %(version)s", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 507bcf49b8..a007f78c1f 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -317,9 +317,9 @@ export const SETTINGS = { displayName: _td('Show empty room list headings'), default: true, }, - "alwaysRetryInvites": { + "alwaysInviteUnknownUsers": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, - displayName: _td('Always retry invites for unknown users'), + displayName: _td('Always invite users which may not exist'), default: false, }, "showDeveloperTools": { diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js index 0d7a8837b8..b5f4f960a9 100644 --- a/src/utils/MultiInviter.js +++ b/src/utils/MultiInviter.js @@ -101,13 +101,18 @@ export default class MultiInviter { if (addrType === 'email') { return MatrixClientPeg.get().inviteByEmail(roomId, addr); } else if (addrType === 'mx-user-id') { - if (!ignoreProfile && !SettingsStore.getValue("alwaysRetryInvites", this.roomId)) { - const profile = await MatrixClientPeg.get().getProfileInfo(addr); - if (!profile) { - return Promise.reject({ - errcode: "M_NOT_FOUND", - error: "User does not have a profile or does not exist.", - }); + if (!ignoreProfile && !SettingsStore.getValue("alwaysInviteUnknownUsers", this.roomId)) { + try { + const profile = await MatrixClientPeg.get().getProfileInfo(addr); + if (!profile) { + // noinspection ExceptionCaughtLocallyJS + throw new Error("User has no profile"); + } + } catch (e) { + throw { + errcode: "RIOT.USER_NOT_FOUND", + error: "User does not have a profile or does not exist." + }; } } @@ -119,6 +124,8 @@ export default class MultiInviter { _doInvite(address, ignoreProfile) { return new Promise((resolve, reject) => { + console.log(`Inviting ${address}`); + let doInvite; if (this.groupId !== null) { doInvite = GroupStore.inviteUserToGroup(this.groupId, address); @@ -151,13 +158,13 @@ export default class MultiInviter { this._doInvite(address, ignoreProfile).then(resolve, reject); }, 5000); return; - } else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND'].includes(err.errcode)) { + } else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'RIOT.USER_NOT_FOUND'].includes(err.errcode)) { errorText = _t("User %(user_id)s does not exist", {user_id: address}); - } else if (err.errcode === 'M_PROFILE_UNKNOWN') { + } else if (err.errcode === 'M_PROFILE_UNDISCLOSED') { errorText = _t("User %(user_id)s may or may not exist", {user_id: address}); } else if (err.errcode === 'M_PROFILE_NOT_FOUND' && !ignoreProfile) { // Invite without the profile check - console.warn(`User ${address} does not have a profile - trying invite again`); + console.warn(`User ${address} does not have a profile - inviting anyways automatically`); this._doInvite(address, true).then(resolve, reject); } else { errorText = _t('Unknown server error'); @@ -188,28 +195,28 @@ export default class MultiInviter { if (Object.keys(this.errors).length > 0 && !this.groupId) { // There were problems inviting some people - see if we can invite them // without caring if they exist or not. - const reinviteErrors = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNKNOWN', 'M_PROFILE_NOT_FOUND']; - const reinvitableUsers = Object.keys(this.errors).filter(a => reinviteErrors.includes(this.errors[a].errcode)); + const unknownProfileErrors = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND', 'RIOT.USER_NOT_FOUND']; + const unknownProfileUsers = Object.keys(this.errors).filter(a => unknownProfileErrors.includes(this.errors[a].errcode)); - if (reinvitableUsers.length > 0) { - const retryInvites = () => { - const promises = reinvitableUsers.map(u => this._doInvite(u, true)); + if (unknownProfileUsers.length > 0) { + const inviteUnknowns = () => { + const promises = unknownProfileUsers.map(u => this._doInvite(u, true)); Promise.all(promises).then(() => this.deferred.resolve(this.completionStates)); }; - if (SettingsStore.getValue("alwaysRetryInvites", this.roomId)) { - retryInvites(); + if (SettingsStore.getValue("alwaysInviteUnknownUsers", this.roomId)) { + inviteUnknowns(); return; } - const RetryInvitesDialog = sdk.getComponent("dialogs.RetryInvitesDialog"); + const AskInviteAnywayDialog = sdk.getComponent("dialogs.AskInviteAnywayDialog"); console.log("Showing failed to invite dialog..."); - Modal.createTrackedDialog('Failed to invite the following users to the room', '', RetryInvitesDialog, { - failedInvites: this.errors, - onTryAgain: () => retryInvites(), + Modal.createTrackedDialog('Failed to invite the following users to the room', '', AskInviteAnywayDialog, { + unknownProfileUsers: unknownProfileUsers.map(u => {return {userId: u, errorText: this.errors[u].errorText};}), + onInviteAnyways: () => inviteUnknowns(), onGiveUp: () => { // Fake all the completion states because we already warned the user - for (const addr of Object.keys(this.completionStates)) { + for (const addr of unknownProfileUsers) { this.completionStates[addr] = 'invited'; } this.deferred.resolve(this.completionStates); @@ -223,7 +230,6 @@ export default class MultiInviter { } const addr = this.addrs[nextIndex]; - console.log(`Inviting ${addr}`); // don't try to invite it if it's an invalid address // (it will already be marked as an error though,