From f34855573ef203182994eac6d9ecaa027225dd48 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 Feb 2019 16:24:26 +0000 Subject: [PATCH 01/46] replace ratelimitedfunc with lodash impl --- src/ratelimitedfunc.js | 58 ++++++++++++------------------------------ 1 file changed, 16 insertions(+), 42 deletions(-) diff --git a/src/ratelimitedfunc.js b/src/ratelimitedfunc.js index 20f6db79b8..1f15f11d91 100644 --- a/src/ratelimitedfunc.js +++ b/src/ratelimitedfunc.js @@ -20,54 +20,28 @@ limitations under the License. * to update the interface once for all of them. * * Note that the function must not take arguments, since the args - * could be different for each invocarion of the function. + * could be different for each invocation of the function. * * The returned function has a 'cancelPendingCall' property which can be called * on unmount or similar to cancel any pending update. */ -module.exports = function(f, minIntervalMs) { - this.lastCall = 0; - this.scheduledCall = undefined; - const self = this; - const wrapper = function() { - const now = Date.now(); +import { throttle } from "lodash"; - if (self.lastCall < now - minIntervalMs) { - f.apply(this); - // get the time again now the function has finished, so if it - // took longer than the delay time to execute, it doesn't - // immediately become eligible to run again. - self.lastCall = Date.now(); - } else if (self.scheduledCall === undefined) { - self.scheduledCall = setTimeout( - () => { - self.scheduledCall = undefined; - f.apply(this); - // get time again as per above - self.lastCall = Date.now(); - }, - (self.lastCall + minIntervalMs) - now, - ); - } +export default function ratelimitedfunc(fn, time) { + const throttledFn = throttle(fn, time, { + leading: true, + trailing: true, + }); + const _bind = throttledFn.bind; + throttledFn.bind = function() { + const boundFn = _bind.apply(throttledFn, arguments); + boundFn.cancelPendingCall = throttledFn.cancelPendingCall; + return boundFn; }; - // add the cancelPendingCall property - wrapper.cancelPendingCall = function() { - if (self.scheduledCall) { - clearTimeout(self.scheduledCall); - self.scheduledCall = undefined; - } + throttledFn.cancelPendingCall = function() { + throttledFn.cancel(); }; - - // make sure that cancelPendingCall is copied when react rebinds the - // wrapper - const _bind = wrapper.bind; - wrapper.bind = function() { - const rebound = _bind.apply(this, arguments); - rebound.cancelPendingCall = wrapper.cancelPendingCall; - return rebound; - }; - - return wrapper; -}; + return throttledFn; +} From d2dd1bae135830f144f962a97fa246cc3ca3b260 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 Feb 2019 16:31:57 +0000 Subject: [PATCH 02/46] run search after you've stopped typing for 200ms instead of every 500ms --- src/components/structures/SearchBox.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index fbcd9a7279..2f777c1163 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -21,7 +21,7 @@ import { _t } from '../../languageHandler'; import { KeyCode } from '../../Keyboard'; import sdk from '../../index'; import dis from '../../dispatcher'; -import rate_limited_func from '../../ratelimitedfunc'; +import { debounce } from 'lodash'; import AccessibleButton from '../../components/views/elements/AccessibleButton'; module.exports = React.createClass({ @@ -67,12 +67,9 @@ module.exports = React.createClass({ this.onSearch(); }, - onSearch: new rate_limited_func( - function() { - this.props.onSearch(this.refs.search.value); - }, - 500, - ), + onSearch: debounce(function() { + this.props.onSearch(this.refs.search.value); + }, 200, {trailing: true}), _onKeyDown: function(ev) { switch (ev.keyCode) { From 60950b258aedf3ff4d874adb27d9037f40d1f71b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 8 Feb 2019 15:23:14 -0700 Subject: [PATCH 03/46] Scale up settings UI to be easier to read Part of https://github.com/vector-im/riot-web/issues/8207 --- res/css/structures/_TabbedView.scss | 19 +++++++++---------- res/css/views/dialogs/_SettingsDialog.scss | 6 +++--- res/css/views/settings/tabs/_SettingsTab.scss | 8 ++++---- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/res/css/structures/_TabbedView.scss b/res/css/structures/_TabbedView.scss index fb4df53d52..6e435b8e75 100644 --- a/res/css/structures/_TabbedView.scss +++ b/res/css/structures/_TabbedView.scss @@ -28,8 +28,8 @@ limitations under the License. } .mx_TabbedView_tabLabels { - width: 150px; - max-width: 150px; + width: 170px; + max-width: 170px; color: $tab-label-fg-color; position: fixed; } @@ -39,9 +39,8 @@ limitations under the License. cursor: pointer; display: block; border-radius: 3px; - font-size: 12px; - font-weight: 600; - min-height: 20px; // use min-height instead of height to allow the label to overflow a bit + font-size: 14px; + min-height: 24px; // use min-height instead of height to allow the label to overflow a bit margin-bottom: 6px; position: relative; } @@ -55,8 +54,8 @@ limitations under the License. margin-left: 6px; margin-right: 9px; margin-top: 1px; - width: 14px; - height: 14px; + width: 16px; + height: 16px; display: inline-block; } @@ -64,9 +63,9 @@ limitations under the License. display: inline-block; background-color: $tab-label-icon-bg-color; mask-repeat: no-repeat; - mask-size: 14px; + mask-size: 16px; width: 14px; - height: 18px; + height: 22px; mask-position: center; content: ''; vertical-align: middle; @@ -81,7 +80,7 @@ limitations under the License. } .mx_TabbedView_tabPanel { - margin-left: 220px; // 150px sidebar + 70px padding + margin-left: 240px; // 170px sidebar + 70px padding flex-grow: 1; display: flex; flex-direction: column; diff --git a/res/css/views/dialogs/_SettingsDialog.scss b/res/css/views/dialogs/_SettingsDialog.scss index 4a9708f6d1..abf0048cfd 100644 --- a/res/css/views/dialogs/_SettingsDialog.scss +++ b/res/css/views/dialogs/_SettingsDialog.scss @@ -16,8 +16,8 @@ limitations under the License. .mx_SettingsDialog { .mx_Dialog { - max-width: 900px; - width: 80%; + max-width: 1000px; + width: 90%; height: 80%; border-radius: 4px; padding-top: 0; @@ -30,7 +30,7 @@ limitations under the License. .mx_TabbedView .mx_SettingsTab { box-sizing: border-box; - min-width: 550px; + min-width: 580px; padding-right: 130px; // Put some padding on the bottom to avoid the settings tab from diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss index a899aec0fa..626c0e32eb 100644 --- a/res/css/views/settings/tabs/_SettingsTab.scss +++ b/res/css/views/settings/tabs/_SettingsTab.scss @@ -21,7 +21,7 @@ limitations under the License. } .mx_SettingsTab_subheading { - font-size: 14px; + font-size: 16px; display: block; font-family: $font-family; font-weight: 600; @@ -32,7 +32,7 @@ limitations under the License. .mx_SettingsTab_subsectionText { color: $settings-subsection-fg-color; - font-size: 12px; + font-size: 14px; padding-bottom: 12px; display: block; margin: 0 100px 0 0; // Align with the rest of the view @@ -45,9 +45,9 @@ limitations under the License. } .mx_SettingsTab_section .mx_SettingsFlag .mx_SettingsFlag_label { - vertical-align: bottom; + vertical-align: middle; display: inline-block; - font-size: 12px; + font-size: 14px; color: $primary-fg-color; max-width: calc(100% - 48px); // Force word wrap instead of colliding with the switch } From 1cf0a6a0490f53864d63ec6c0a3a87f2f2f7f7ce Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 11 Feb 2019 12:08:10 +0000 Subject: [PATCH 04/46] Add legacy verification button on wait Add a way to bail out of interactive verification from the screen where you're waiting for the other person to accept. --- .../views/dialogs/DeviceVerifyDialog.js | 15 +++++++++++++++ src/i18n/strings/en_EN.json | 1 + 2 files changed, 16 insertions(+) diff --git a/src/components/views/dialogs/DeviceVerifyDialog.js b/src/components/views/dialogs/DeviceVerifyDialog.js index 47ee73c61c..269facd107 100644 --- a/src/components/views/dialogs/DeviceVerifyDialog.js +++ b/src/components/views/dialogs/DeviceVerifyDialog.js @@ -60,6 +60,11 @@ export default class DeviceVerifyDialog extends React.Component { } _onSwitchToLegacyClick = () => { + if (this._verifier) { + this._verifier.removeListener('show_sas', this._onVerifierShowSas); + this._verifier.cancel('User cancel'); + this._verifier = null; + } this.setState({mode: MODE_LEGACY}); } @@ -184,11 +189,21 @@ export default class DeviceVerifyDialog extends React.Component { _renderSasVerificationPhaseWaitAccept() { const Spinner = sdk.getComponent("views.elements.Spinner"); + const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); return (

{_t("Waiting for partner to accept...")}

+

{_t( + "Nothing appearing? Not all clients support interactive verification yet. " + + ".", + {}, {button: sub => + {sub} + } + )}

); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f48e306641..3cc6e71436 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1061,6 +1061,7 @@ "Verify by comparing a short text string.": "Verify by comparing a short text string.", "Begin Verifying": "Begin Verifying", "Waiting for partner to accept...": "Waiting for partner to accept...", + "Nothing appearing? Not all clients support interactive verification yet. .": "Nothing appearing? Not all clients support interactive verification yet. .", "Waiting for %(userId)s to confirm...": "Waiting for %(userId)s to confirm...", "Use two-way text verification": "Use two-way text verification", "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:": "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:", From 97aa0b52fad7a0fdbd63bd183bf4c107545b3834 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 11 Feb 2019 12:12:47 +0000 Subject: [PATCH 05/46] Avoid room directory error without a client --- src/components/structures/RoomDirectory.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 5309b02041..e13eab8eb3 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -78,6 +78,11 @@ module.exports = React.createClass({ this.protocols = null; this.setState({protocolsLoading: true}); + if (!MatrixClientPeg.get()) { + // We may not have a client yet when invoked from welcome page + this.setState({protocolsLoading: false}); + return; + } MatrixClientPeg.get().getThirdpartyProtocols().done((response) => { this.protocols = response; this.setState({protocolsLoading: false}); From ba597c65eba04d06b48a41a41f148e619b2cc400 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 11 Feb 2019 13:09:50 +0000 Subject: [PATCH 06/46] lint --- src/components/views/dialogs/DeviceVerifyDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/DeviceVerifyDialog.js b/src/components/views/dialogs/DeviceVerifyDialog.js index 269facd107..c901942fcd 100644 --- a/src/components/views/dialogs/DeviceVerifyDialog.js +++ b/src/components/views/dialogs/DeviceVerifyDialog.js @@ -202,7 +202,7 @@ export default class DeviceVerifyDialog extends React.Component { onClick={this._onSwitchToLegacyClick} > {sub} - } + }, )}

); From 119806bdbd8494724206ca3112c4f3410f6c80cf Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 11 Feb 2019 13:31:06 +0000 Subject: [PATCH 07/46] !important shouldn't have a space I wonder if this is making SCSS choke. --- res/css/views/rooms/_RoomTile.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 220790784e..2024a503ae 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -154,7 +154,7 @@ limitations under the License. } .mx_RoomTile_unread, .mx_RoomTile_highlight { - font-weight: 700 ! important; + font-weight: 700 !important; .mx_RoomTile_name { color: $roomtile-selected-color; @@ -176,7 +176,7 @@ limitations under the License. } .mx_RoomTile:focus { - filter: none ! important; + filter: none !important; background-color: $roomtile-focused-bg-color; } From 477d5ac0dcc612202fba4b04d10da290c479a468 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 11 Feb 2019 13:31:27 +0000 Subject: [PATCH 08/46] View welcome behind modals when not showing logged in view --- src/components/structures/MatrixChat.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 1453007b18..7c03aaec57 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -574,11 +574,8 @@ export default React.createClass({ const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog"); Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {}, 'mx_SettingsDialog'); - // View the home page if we need something to look at - if (!this.state.currentGroupId && !this.state.currentRoomId) { - this._setPage(PageTypes.HomePage); - this.notifyNewScreen('home'); - } + // View the welcome or home page if we need something to look at + this._viewSomethingBehindModal(); break; } case 'view_create_room': @@ -595,11 +592,8 @@ export default React.createClass({ config: this.props.config, }, 'mx_RoomDirectory_dialogWrapper'); - // View the home page if we need something to look at - if (!this.state.currentGroupId && !this.state.currentRoomId) { - this._setPage(PageTypes.HomePage); - this.notifyNewScreen('home'); - } + // View the welcome or home page if we need something to look at + this._viewSomethingBehindModal(); } break; case 'view_my_groups': @@ -887,6 +881,16 @@ export default React.createClass({ this.notifyNewScreen('group/' + groupId); }, + _viewSomethingBehindModal() { + if (this.state.view !== VIEWS.LOGGED_IN) { + this._viewWelcome(); + return; + } + if (!this.state.currentGroupId && !this.state.currentRoomId) { + this._viewHome(); + } + }, + _viewWelcome() { this.setStateForNewView({ view: VIEWS.WELCOME, From d66dbf9be79feed80295080f3088852138235316 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 11 Feb 2019 13:38:47 +0000 Subject: [PATCH 09/46] Change taking a community off the left-left panel less scary With the current wording, I half-think clicking this button might remove me from the community! IMO 'Hide' sounds more like it's just going to disappear from the panel, but I can add it back at some point. --- src/components/views/context_menus/TagTileContextMenu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/context_menus/TagTileContextMenu.js b/src/components/views/context_menus/TagTileContextMenu.js index 8b868e7b11..c0203a3ac8 100644 --- a/src/components/views/context_menus/TagTileContextMenu.js +++ b/src/components/views/context_menus/TagTileContextMenu.js @@ -68,7 +68,7 @@ export default class TagTileContextMenu extends React.Component {
- { _t('Remove') } + { _t('Hide') }
; } From 4101d4e9de0e4735106930bc7b8a0366901c56fd Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 11 Feb 2019 13:49:18 +0000 Subject: [PATCH 10/46] Always change to LOGGED_IN view to show a room --- src/components/structures/MatrixChat.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 7c03aaec57..4bb4e34033 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -819,6 +819,7 @@ export default React.createClass({ this.focusComposer = true; const newState = { + view: VIEWS.LOGGED_IN, currentRoomId: roomInfo.room_id || null, page_type: PageTypes.RoomView, thirdPartyInvite: roomInfo.third_party_invite, @@ -1556,11 +1557,7 @@ export default React.createClass({ payload.room_id = roomString; } - // we can't view a room unless we're logged in - // (a guest account is fine) - if (this.state.view === VIEWS.LOGGED_IN) { - dis.dispatch(payload); - } + dis.dispatch(payload); } else if (screen.indexOf('user/') == 0) { const userId = screen.substring(5); From 9d3ba2b3d9a9fcc0f8ee1fc222c9da9d26ce3d25 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 11 Feb 2019 14:33:19 +0000 Subject: [PATCH 11/46] Guard against invalid room object on join --- src/components/structures/RoomView.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 50fa18e075..f75393c6db 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -877,13 +877,12 @@ module.exports = React.createClass({ // If the user is a ROU, allow them to transition to a PWLU if (cli && cli.isGuest()) { // Join this room once the user has registered and logged in - const signUrl = this.props.thirdPartyInvite ? - this.props.thirdPartyInvite.inviteSignUrl : undefined; + // (If we failed to peek, we may not have a valid room object.) dis.dispatch({ action: 'do_after_sync_prepared', deferred_action: { action: 'view_room', - room_id: this.state.room.roomId, + room_id: this.state.room ? this.state.room.roomId : this.state.roomId, }, }); From b6aa72da55e2db5d0532dd8f11edd277430d0863 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 11 Feb 2019 15:40:17 +0100 Subject: [PATCH 12/46] update RoomTile notificationCount through props so updates are consistent --- src/components/structures/RoomSubList.js | 1 + src/components/views/rooms/RoomTile.js | 11 +---------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 4644d19f61..ca2be85b35 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -145,6 +145,7 @@ const RoomSubList = React.createClass({ collapsed={this.props.collapsed || false} unread={Unread.doesRoomHaveUnreadMessages(room)} highlight={room.getUnreadNotificationCount('highlight') > 0 || this.props.isInvite} + notificationCount={room.getUnreadNotificationCount()} isInvite={this.props.isInvite} refreshSubList={this._updateSubListCount} incomingCall={null} diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index ed214812b5..f9e9d64b9e 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -108,13 +108,6 @@ module.exports = React.createClass({ return statusUser._unstable_statusMessage; }, - onRoomTimeline: function(ev, room) { - if (room !== this.props.room) return; - this.setState({ - notificationCount: this.props.room.getUnreadNotificationCount(), - }); - }, - onRoomName: function(room) { if (room !== this.props.room) return; this.setState({ @@ -159,7 +152,6 @@ module.exports = React.createClass({ componentWillMount: function() { MatrixClientPeg.get().on("accountData", this.onAccountData); - MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().on("Room.name", this.onRoomName); ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange); this.dispatcherRef = dis.register(this.onAction); @@ -179,7 +171,6 @@ module.exports = React.createClass({ const cli = MatrixClientPeg.get(); if (cli) { MatrixClientPeg.get().removeListener("accountData", this.onAccountData); - MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); } ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange); @@ -306,7 +297,7 @@ module.exports = React.createClass({ render: function() { const isInvite = this.props.room.getMyMembership() === "invite"; - const notificationCount = this.state.notificationCount; + const notificationCount = this.props.notificationCount; // var highlightCount = this.props.room.getUnreadNotificationCount("highlight"); const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge(); From d069f4a89a4057d5107d1b2452068426d51eefc6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 11 Feb 2019 15:41:32 +0100 Subject: [PATCH 13/46] not the cause of the bug, but this seems wrong --- src/Unread.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Unread.js b/src/Unread.js index 55e60f2a9a..9514ec821b 100644 --- a/src/Unread.js +++ b/src/Unread.js @@ -32,7 +32,7 @@ module.exports = { return false; } else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') { return false; - } else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') { + } else if (ev.getType() == 'm.room.message' && ev.getContent().msgtype == 'm.notify') { return false; } const EventTile = sdk.getComponent('rooms.EventTile'); From fbc0bbfb6f7e80639c9d5288df632ead62938244 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 11 Feb 2019 16:17:15 +0100 Subject: [PATCH 14/46] clear hover flag after 1000ms of not moving the mouse as a fix for #8184 --- src/components/views/rooms/RoomList.js | 30 +++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 56eb4b713d..1d207835bc 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -17,6 +17,7 @@ limitations under the License. 'use strict'; import SettingsStore from "../../../settings/SettingsStore"; +import Timer from "../../../utils/Timer"; const React = require("react"); const ReactDOM = require("react-dom"); @@ -41,6 +42,7 @@ import {Resizer} from '../../../resizer'; import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2'; const HIDE_CONFERENCE_CHANS = true; const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; +const HOVER_MOVE_TIMEOUT = 1000; function labelForTagName(tagName) { if (tagName.startsWith('u.')) return tagName.slice(2); @@ -73,6 +75,7 @@ module.exports = React.createClass({ getInitialState: function() { + this._hoverClearTimer = null; this._subListRefs = { // key => RoomSubList ref }; @@ -357,11 +360,32 @@ module.exports = React.createClass({ this.forceUpdate(); }, - onMouseEnter: function(ev) { - this.setState({hover: true}); + onMouseMove: async function(ev) { + if (!this._hoverClearTimer) { + this.setState({hover: true}); + this._hoverClearTimer = new Timer(HOVER_MOVE_TIMEOUT); + this._hoverClearTimer.start(); + let finished = true; + try { + await this._hoverClearTimer.finished(); + } catch (err) { + finished = false; + } + this._hoverClearTimer = null; + if (finished) { + this.setState({hover: false}); + this._delayedRefreshRoomList(); + } + } else { + this._hoverClearTimer.restart(); + } }, onMouseLeave: function(ev) { + if (this._hoverClearTimer) { + this._hoverClearTimer.abort(); + this._hoverClearTimer = null; + } this.setState({hover: false}); // Refresh the room list just in case the user missed something. @@ -774,7 +798,7 @@ module.exports = React.createClass({ return (
+ onMouseMove={this.onMouseMove} onMouseLeave={this.onMouseLeave}> { subListComponents }
); From ab6535b1353b18f7c03bc141e74aed8dd956fe3d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 11 Feb 2019 16:27:29 +0100 Subject: [PATCH 15/46] lint --- src/components/views/rooms/RoomList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 1d207835bc..227dd318ed 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -98,7 +98,7 @@ module.exports = React.createClass({ // update overflow indicators this._checkSubListsOverflow(); // don't store height for collapsed sublists - if(!this.collapsedState[key]) { + if (!this.collapsedState[key]) { this.subListSizes[key] = size; window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.subListSizes)); From 99ae63c0218c44afc5bb75e363b70ec8f50b108c Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 11 Feb 2019 15:57:34 +0000 Subject: [PATCH 16/46] Add display name / avatar to incoming sas dialog Fetch the other user's profile & display it on an incoming verification request --- res/css/_components.scss | 1 + res/css/views/dialogs/_IncomingSasDialog.scss | 24 +++++++++ .../views/dialogs/IncomingSasDialog.js | 50 ++++++++++++++++++- 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 res/css/views/dialogs/_IncomingSasDialog.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index 57a34023c0..80add0b1ad 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -57,6 +57,7 @@ @import "./views/dialogs/_DevtoolsDialog.scss"; @import "./views/dialogs/_EncryptedEventDialog.scss"; @import "./views/dialogs/_GroupAddressPicker.scss"; +@import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_RestoreKeyBackupDialog.scss"; @import "./views/dialogs/_RoomSettingsDialog.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss"; diff --git a/res/css/views/dialogs/_IncomingSasDialog.scss b/res/css/views/dialogs/_IncomingSasDialog.scss new file mode 100644 index 0000000000..3a9d645a98 --- /dev/null +++ b/res/css/views/dialogs/_IncomingSasDialog.scss @@ -0,0 +1,24 @@ +/* +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. +*/ + +.mx_IncomingSasDialog_opponentProfile_image { + position: relative; +} + +.mx_IncomingSasDialog_opponentProfile h2 { + display: inline-block; + margin-left: 10px; +} diff --git a/src/components/views/dialogs/IncomingSasDialog.js b/src/components/views/dialogs/IncomingSasDialog.js index 2a76e8a904..da2211c10f 100644 --- a/src/components/views/dialogs/IncomingSasDialog.js +++ b/src/components/views/dialogs/IncomingSasDialog.js @@ -16,6 +16,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import MatrixClientPeg from '../../../MatrixClientPeg'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; @@ -37,9 +38,12 @@ export default class IncomingSasDialog extends React.Component { this.state = { phase: PHASE_START, sasVerified: false, + opponentProfile: null, + opponentProfileError: null, }; this.props.verifier.on('show_sas', this._onVerifierShowSas); this.props.verifier.on('cancel', this._onVerifierCancel); + this._fetchOpponentProfile(); } componentWillUnmount() { @@ -49,6 +53,21 @@ export default class IncomingSasDialog extends React.Component { this.props.verifier.removeListener('show_sas', this._onVerifierShowSas); } + async _fetchOpponentProfile() { + try { + const prof = await MatrixClientPeg.get().getProfileInfo( + this.props.verifier.userId, + ); + this.setState({ + opponentProfile: prof, + }); + } catch (e) { + this.setState({ + opponentProfileError: e, + }); + } + } + _onFinished = () => { this.props.onFinished(this.state.phase === PHASE_VERIFIED); } @@ -93,10 +112,39 @@ export default class IncomingSasDialog extends React.Component { _renderPhaseStart() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + const Spinner = sdk.getComponent("views.elements.Spinner"); + const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); + + let profile; + if (this.state.opponentProfile) { + profile =
+ +

{this.state.opponentProfile.displayname}

+
; + } else if (this.state.opponentProfileError) { + profile =
+ +

{this.props.verifier.userId}

+
; + } else { + profile = ; + } return (
-

{this.props.verifier.userId}

+ {profile}

{_t( "Verify this user to mark them as trusted. " + "Trusting users gives you extra peace of mind when using " + From 6aef9fcc75ec7a1a6b6439c9563b3b361cefb254 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 11 Feb 2019 16:41:06 +0000 Subject: [PATCH 17/46] Restore backup on new recovery method dialog Rather than verifying --- .../keybackup/NewRecoveryMethodDialog.js | 37 +------------------ 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js index db86178b5a..28281af771 100644 --- a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js +++ b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js @@ -39,36 +39,8 @@ export default class NewRecoveryMethodDialog extends React.PureComponent { } 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 - // look for an unverified device and verify it. Note that this means - // we won't restore keys yet; instead we'll only trust the backup for - // sending our own new keys to it. - let backupSigStatus; - try { - backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(this.props.newVersionInfo); - } catch (e) { - console.log("Unable to fetch key backup status", e); - return; - } - - let unverifiedDevice; - for (const sig of backupSigStatus.sigs) { - if (!sig.device.isVerified()) { - unverifiedDevice = sig.device; - break; - } - } - if (!unverifiedDevice) { - console.log("Unable to find a device to verify."); - return; - } - - const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog'); - Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, { - userId: MatrixClientPeg.get().credentials.userId, - device: unverifiedDevice, + const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); + Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, { onFinished: this.props.onFinished, }); } @@ -111,11 +83,6 @@ export default class NewRecoveryMethodDialog extends React.PureComponent { } 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} Date: Tue, 12 Feb 2019 08:59:38 +0000 Subject: [PATCH 18/46] Explain roomId workaround --- src/components/structures/RoomView.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index f75393c6db..b233662898 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -283,6 +283,15 @@ module.exports = React.createClass({ } }, + _getRoomId() { + // According to `_onRoomViewStoreUpdate`, `state.roomId` can be null + // if we have a room alias we haven't resolved yet. To work around this, + // first we'll try the room object if it's there, and then fallback to + // the bare room ID. (We may want to update `state.roomId` after + // resolving aliases, so we could always trust it.) + return this.state.room ? this.state.room.roomId : this.state.roomId; + }, + _onWidgetEchoStoreUpdate: function() { this.setState({ showApps: this._shouldShowApps(this.state.room), @@ -882,7 +891,7 @@ module.exports = React.createClass({ action: 'do_after_sync_prepared', deferred_action: { action: 'view_room', - room_id: this.state.room ? this.state.room.roomId : this.state.roomId, + room_id: this._getRoomId(), }, }); From aaea40a93d7f684b3c4c1593c5ab99730b766285 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 12 Feb 2019 11:04:25 +0100 Subject: [PATCH 19/46] add breadcrumbs component --- res/css/_components.scss | 1 + res/css/views/rooms/_RoomBreadcrumbs.scss | 47 +++++++++++ src/components/structures/LeftPanel.js | 2 + src/components/views/rooms/RoomBreadcrumbs.js | 84 +++++++++++++++++++ 4 files changed, 134 insertions(+) create mode 100644 res/css/views/rooms/_RoomBreadcrumbs.scss create mode 100644 src/components/views/rooms/RoomBreadcrumbs.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 57a34023c0..62c6186152 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -126,6 +126,7 @@ @import "./views/rooms/_PinnedEventsPanel.scss"; @import "./views/rooms/_PresenceLabel.scss"; @import "./views/rooms/_ReplyPreview.scss"; +@import "./views/rooms/_RoomBreadcrumbs.scss"; @import "./views/rooms/_RoomDropTarget.scss"; @import "./views/rooms/_RoomHeader.scss"; @import "./views/rooms/_RoomList.scss"; diff --git a/res/css/views/rooms/_RoomBreadcrumbs.scss b/res/css/views/rooms/_RoomBreadcrumbs.scss new file mode 100644 index 0000000000..41149fc0b2 --- /dev/null +++ b/res/css/views/rooms/_RoomBreadcrumbs.scss @@ -0,0 +1,47 @@ +/* +Copyright 2015, 2016 OpenMarket 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_RoomBreadcrumbs { + overflow-x: auto; + position: relative; + height: 32px; + margin: 8px; + margin-bottom: 0; + + > div { + display: flex; + flex-direction: row; + position: absolute; + right: 0; + top: 0; + height: 32px; + + > * { + margin-right: 4px; + } + } + + &::after { + content: ""; + position: absolute; + width: 15px; + top: 0; + left: 0; + height: 100%; + background: linear-gradient(to left, rgba(242,245,248,0), rgba(242,245,248,1)); + } +} + diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index bd49f8acd4..49956265f6 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -182,6 +182,7 @@ const LeftPanel = React.createClass({ render: function() { const RoomList = sdk.getComponent('rooms.RoomList'); + const RoomBreadcrumbs = sdk.getComponent('rooms.RoomBreadcrumbs'); const TagPanel = sdk.getComponent('structures.TagPanel'); const CustomRoomTagPanel = sdk.getComponent('structures.CustomRoomTagPanel'); const TopLeftMenuButton = sdk.getComponent('structures.TopLeftMenuButton'); @@ -221,6 +222,7 @@ const LeftPanel = React.createClass({ { tagPanelContainer }