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 155d3d1b23..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 16 --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", @@ -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/res/css/_components.scss b/res/css/_components.scss index d8f966603d..bdcf27ac16 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..541052175d --- /dev/null +++ b/res/css/structures/_GroupGridView.scss @@ -0,0 +1,130 @@ +/* +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: flex; + flex-direction: column; +} + +.mx_GroupGridView_rooms { + display: grid; + 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 { + 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; +} + +.mx_GroupGridView_emptyTile { + display: block; + margin-top: 100px; + text-align: center; + user-select: none; +} + +.mx_GroupGridView_tile { + border-right: 1px solid $panel-divider-color; + border-bottom: 1px solid $panel-divider-color; +} + +.mx_GroupGridView_activeTile { + 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: 3500; +} + +.mx_GroupGridView_activeTile:before { + border-radius: 14px; + border: 8px solid $gridview-focus-border-glow-color; + margin: -8px; +} + +.mx_GroupGridView_activeTile:after { + border-radius: 8px; + border: 2px solid $gridview-focus-border-color; + margin: -2px; +} + +.mx_GroupGridView_tile > .mx_RoomView { + height: 100%; +} + +.mx_GroupGridView_rooms > *:nth-child(1) { + grid-column: 1; + grid-row: 1; +} + +.mx_GroupGridView_rooms > *:nth-child(2) { + grid-column: 2; + grid-row: 1; +} + +.mx_GroupGridView_rooms > *:nth-child(3) { + grid-column: 3; + grid-row: 1; +} + +.mx_GroupGridView_rooms > *:nth-child(4) { + grid-column: 1; + grid-row: 2; +} + +.mx_GroupGridView_rooms > *:nth-child(5) { + grid-column: 2; + grid-row: 2; +} + +.mx_GroupGridView_rooms > *:nth-child(6) { + grid-column: 3; + grid-row: 2; +} diff --git a/res/css/structures/_MatrixChat.scss b/res/css/structures/_MatrixChat.scss index 1ccbd19391..6d8b79ecb2 100644 --- a/res/css/structures/_MatrixChat.scss +++ b/res/css/structures/_MatrixChat.scss @@ -73,14 +73,16 @@ 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. 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 diff --git a/res/css/views/rooms/_MemberList.scss b/res/css/views/rooms/_MemberList.scss index 6f9491b22f..567727fb64 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/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/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 997a74e6aa..257b723ccf 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); $imagebody-giflabel-color: rgba(0, 0, 0, 1); diff --git a/res/themes/dharma/css/_dharma.scss b/res/themes/dharma/css/_dharma.scss index 54ccbc7dd0..732cabf494 100644 --- a/res/themes/dharma/css/_dharma.scss +++ b/res/themes/dharma/css/_dharma.scss @@ -184,6 +184,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 bd445bbc05..10a8fcd1e5 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); $imagebody-giflabel-color: rgba(255, 255, 255, 1); diff --git a/src/ActiveRoomObserver.js b/src/ActiveRoomObserver.js index d6fbb460b5..c276cccb5d 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,15 @@ import RoomViewStore from './stores/RoomViewStore'; class ActiveRoomObserver { constructor() { this._listeners = {}; - - this._activeRoomId = RoomViewStore.getRoomId(); + 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. - this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate.bind(this)); + this._roomStoreToken = OpenRoomsStore.addListener(this._onOpenRoomsStoreUpdate.bind(this)); + } + + getActiveRoomId() { + return this._activeRoomId; } addListener(roomId, listener) { @@ -51,23 +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); } } - _onRoomViewStoreUpdate() { + _onOpenRoomsStoreUpdate() { + const activeRoomStore = OpenRoomsStore.getActiveRoomStore(); + const newActiveRoomId = activeRoomStore && activeRoomStore.getRoomId(); // emit for the old room ID - if (this._activeRoomId) this._emit(this._activeRoomId); - + if (this._activeRoomId) this._emit(this._activeRoomId, newActiveRoomId); // update our cache - this._activeRoomId = RoomViewStore.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); } } 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/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/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 64e0702012..c5a7ff558d 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, }); @@ -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."); } @@ -352,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")}
@@ -369,7 +372,7 @@ export default React.createClass({
-

+ ; }, @@ -405,7 +408,6 @@ export default React.createClass({ _renderBusyPhase: function(text) { const Spinner = sdk.getComponent('views.elements.Spinner'); return
-

{_t(text)}

; }, @@ -413,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.")}

{ + 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/GroupGridView.js b/src/components/structures/GroupGridView.js new file mode 100644 index 0000000000..a1a9e1b183 --- /dev/null +++ b/src/components/structures/GroupGridView.js @@ -0,0 +1,127 @@ +/* +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 OpenRoomsStore from '../../stores/OpenRoomsStore'; +import dis from '../../dispatcher'; +import {_t} from '../../languageHandler'; +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 { + constructor(props) { + 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); + } + + componentWillUnmount() { + this._unmounted = true; + if (this._openRoomsStoreRegistration) { + this._openRoomsStoreRegistration.remove(); + } + } + + onRoomsChanged() { + if (this._unmounted) return; + this.setState({ + roomStores: OpenRoomsStore.getRoomStores(), + activeRoomStore: OpenRoomsStore.getActiveRoomStore(), + }); + } + + _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; + if (emptyCount) { + const emptyTiles = Array.from({length: emptyCount}, () => null); + roomStores = roomStores.concat(emptyTiles); + } + const activeRoomId = this.state.activeRoomStore && this.state.activeRoomStore.getRoomId(); + let rightPanel; + if (activeRoomId) { + rightPanel = ( +
+
+ +
+ ); + } + + 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 (
{_t("No room in this tile yet.")}
); + } + }) } +
+
+
); + } +} 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/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 0433ce25b3..67d7d41701 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,7 +430,14 @@ const LoggedInView = React.createClass({ switch (this.props.page_type) { case PageTypes.RoomView: + if (!OpenRoomsStore.getActiveRoomStore()) { + console.warn(`LoggedInView: getCurrentRoomStore not set!`); + } + 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 = ; break; - + case PageTypes.GroupGridView: + page_element = ; + break; case PageTypes.UserSettings: page_element = ; } else if (this.state.phase === RightPanel.Phase.RoomMemberInfo) { - panel = ; + panel = ; } else if (this.state.phase === RightPanel.Phase.GroupMemberInfo) { panel = { - 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; @@ -922,7 +921,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 }, }); @@ -987,10 +986,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; } @@ -1014,14 +1013,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; } @@ -1222,7 +1221,7 @@ module.exports = React.createClass({ }, onSettingsClick: function() { - dis.dispatch({ action: 'open_room_settings' }); + this.props.roomViewStore.getDispatcher().dispatch({ action: 'open_room_settings' }); }, onSettingsSaveClick: function() { @@ -1255,31 +1254,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, }); @@ -1287,7 +1286,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"); @@ -1304,7 +1303,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, }); @@ -1330,7 +1329,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', }); }, @@ -1349,7 +1348,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 @@ -1439,7 +1438,7 @@ module.exports = React.createClass({ }, onFullscreenClick: function() { - dis.dispatch({ + this.props.roomViewStore.getDispatcher().dispatch({ action: 'video_fullscreen', fullscreen: true, }, true); @@ -1564,6 +1563,7 @@ module.exports = React.createClass({
@@ -1610,6 +1610,7 @@ module.exports = React.createClass({
@@ -1751,7 +1752,9 @@ module.exports = React.createClass({ if (canSpeak) { messageComposer = : undefined; + const rightPanel = this.state.room && !this.props.isGrid ? + : + undefined; return (
{ // 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/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: ""}); }; diff --git a/src/components/views/context_menus/TagTileContextMenu.js b/src/components/views/context_menus/TagTileContextMenu.js index 32f5365b82..8ce09bdb2c 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 = { @@ -34,6 +35,7 @@ export default class TagTileContextMenu extends React.Component { this._onViewCommunityClick = this._onViewCommunityClick.bind(this); this._onRemoveClick = this._onRemoveClick.bind(this); + this._onViewAsGridClick = this._onViewAsGridClick.bind(this); } _onViewCommunityClick() { @@ -53,8 +55,28 @@ export default class TagTileContextMenu extends React.Component { this.props.onFinished(); } + _onViewAsGridClick() { + dis.dispatch({ + action: 'group_grid_view', + group_id: this.props.tag, + }); + this.props.onFinished(); + } + render() { const TintableSvg = sdk.getComponent("elements.TintableSvg"); + let gridViewOption; + if (SettingsStore.isFeatureEnabled("feature_gridview")) { + gridViewOption = (
+ + { _t('View as Grid') } +
); + } return
{ _t('View Community') }
+ { gridViewOption }
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/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 12dc2117a0..2b50ff5e48 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -39,7 +39,6 @@ import Unread from '../../../Unread'; import { findReadReceiptFromUserId } from '../../../utils/Receipt'; import withMatrixClient from '../../../wrappers/withMatrixClient'; import AccessibleButton from '../elements/AccessibleButton'; -import RoomViewStore from '../../../stores/RoomViewStore'; import SdkConfig from '../../../SdkConfig'; import MultiInviter from "../../../utils/MultiInviter"; import SettingsStore from "../../../settings/SettingsStore"; @@ -50,6 +49,7 @@ module.exports = withMatrixClient(React.createClass({ propTypes: { matrixClient: PropTypes.object.isRequired, member: PropTypes.object.isRequired, + roomId: PropTypes.string, }, getInitialState: function() { @@ -713,8 +713,8 @@ module.exports = withMatrixClient(React.createClass({ } if (!member || !member.membership || member.membership === 'leave') { - const roomId = member && member.roomId ? member.roomId : RoomViewStore.getRoomId(); - const onInviteUserButton = async() => { + const roomId = member && member.roomId ? member.roomId : this.props.roomId; + 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/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index d4b607a93a..e15ca047ac 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,14 +123,14 @@ 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 }); } onUploadClick(ev) { if (MatrixClientPeg.get().isGuest()) { - dis.dispatch({action: 'require_registration'}); + this.props.roomViewStore.getDispatcher().dispatch({action: 'require_registration'}); return; } @@ -165,7 +164,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 =

{ @@ -229,7 +228,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) @@ -238,7 +237,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, @@ -246,7 +245,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, @@ -282,7 +281,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, @@ -421,8 +420,10 @@ export default class MessageComposer extends React.Component { controls.push( this.messageComposerInput = c} key="controls_input" + isGrid={this.props.isGrid} onResize={this.props.onResize} room={this.props.room} placeholder={placeholderText} @@ -529,5 +530,6 @@ MessageComposer.propTypes = { uploadAllowed: PropTypes.func.isRequired, // string representing the current room app drawer state - showApps: PropTypes.bool + 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..80f90d37b4 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'; @@ -58,7 +56,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'; @@ -121,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', }); } @@ -135,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 */ @@ -150,6 +159,7 @@ export default class MessageComposerInput extends React.Component { onFilesPasted: PropTypes.func, onInputStateChanged: PropTypes.func, + roomViewStore: PropTypes.object.isRequired, }; client: MatrixClient; @@ -344,12 +354,16 @@ export default class MessageComposerInput extends React.Component { } componentWillMount() { - this.dispatcherRef = dis.register(this.onAction); - this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_'); + 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_'); + } } componentWillUnmount() { - dis.unregister(this.dispatcherRef); + this.props.roomViewStore.getDispatcher().unregister(this.dispatcherRef); } _collectEditor = (e) => { @@ -1120,7 +1134,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) { @@ -1208,14 +1222,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) => { @@ -1260,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 @@ -1308,7 +1322,7 @@ export default class MessageComposerInput extends React.Component { return true; }; - onTab = async(e) => { + onTab = async (e) => { this.setState({ someCompletions: null, }); @@ -1330,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); @@ -1349,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) { @@ -1589,7 +1603,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 }); } diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 4292fa6a4d..91ca73dd59 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -24,6 +24,7 @@ import { _t } from '../../../languageHandler'; import MatrixClientPeg from '../../../MatrixClientPeg'; import Modal from "../../../Modal"; import RateLimitedFunc from '../../../ratelimitedfunc'; +import dis from '../../../dispatcher'; import * as linkify from 'linkifyjs'; import linkifyElement from 'linkifyjs/element'; @@ -152,6 +153,14 @@ module.exports = React.createClass({ }); }, + onToggleRightPanelClick: function(ev) { + if (this.props.collapsedRhs) { + dis.dispatch({action: "show_right_panel"}); + } else { + dis.dispatch({action: "hide_right_panel"}); + } + }, + _hasUnreadPins: function() { const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", ''); if (!currentPinEvent) return false; @@ -409,6 +418,17 @@ module.exports = React.createClass({
; } + let toggleRightPanelButton; + if (this.props.isGrid) { + toggleRightPanelButton = + + + ; + } + return (
@@ -419,7 +439,8 @@ module.exports = React.createClass({ { saveButton } { cancelButton } { rightRow } - + { !this.props.isGrid ? : undefined } + { toggleRightPanelButton }
); diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index bce4d15f16..95073b7be8 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'; import SettingsStore from "../../../settings/SettingsStore"; module.exports = React.createClass({ @@ -62,7 +61,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(), }); }, @@ -117,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, }); }, diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 050c726ba4..b1c94c3067 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -21,13 +21,15 @@ 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); 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.Component { loading: true, error: null, backupInfo: null, + sessionsRemaining: 0, }; } @@ -43,6 +46,10 @@ export default class KeyBackupPanel extends React.Component { 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.Component { 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(); } @@ -144,15 +161,30 @@ export default class KeyBackupPanel extends React.Component { } else if (this.state.backupInfo) { let clientBackupStatus; if (MatrixClientPeg.get().getKeyBackupEnabled()) { - clientBackupStatus = _t("This device is uploading keys to this backup"); + clientBackupStatus = _t("This device is using key backup"); } else { // XXX: display why and how to fix it clientBackupStatus = _t( - "This device is not uploading keys to this backup", {}, + "This device is not using key backup", {}, {b: x => {x}}, ); } + let uploadStatus; + const { sessionsRemaining } = this.state; + 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")}
+
; + } + let backupSigStatuses = this.state.backupSigStatus.sigs.map((sig, i) => { const deviceName = sig.device.getDisplayName() || sig.device.deviceId; const validity = sub => @@ -217,6 +249,7 @@ export default class KeyBackupPanel extends React.Component { {_t("Backup version: ")}{this.state.backupInfo.version}
{_t("Algorithm: ")}{this.state.backupInfo.algorithm}
{clientBackupStatus}
+ {uploadStatus}
{backupSigStatuses}


{...}. - * @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/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ef1b2e9162..25cc3a299e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -352,8 +352,10 @@ "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history": "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history", "Delete backup": "Delete backup", "Unable to load key backup status": "Unable to load key backup status", - "This device is uploading keys to this backup": "This device is uploading keys to this backup", - "This device is 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", "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 ", @@ -1386,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", @@ -1408,5 +1410,8 @@ "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", + "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." } 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); + } + } +} 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'), diff --git a/src/stores/OpenRoomsStore.js b/src/stores/OpenRoomsStore.js new file mode 100644 index 0000000000..21f02fe28d --- /dev/null +++ b/src/stores/OpenRoomsStore.js @@ -0,0 +1,277 @@ +/* +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 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. + */ +class OpenRoomsStore extends Store { + constructor() { + super(dis); + + // Initialise state + this._state = { + rooms: [], + currentIndex: null, + group_id: null, + }; + + this._forwardingEvent = null; + } + + getRoomStores() { + return this._state.rooms.map((r) => r.store); + } + + getActiveRoomStore() { + const openRoom = this._getActiveOpenRoom(); + if (openRoom) { + return openRoom.store; + } + } + + getRoomStoreAt(index) { + if (index >= 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) { + return this._state.rooms[index]; + } + } + + _setState(newState) { + this._state = Object.assign(this._state, newState); + this.__emitChange(); + } + + _hasRoom(payload) { + return this._roomIndex(payload) !== -1; + } + + _roomIndex(payload) { + return this._state.rooms.findIndex((r) => matchesRoom(payload, r.store)); + } + + _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, + }); + } + + _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' && roomId === payload.room_id) { + return; + } + payload.grid_src_room_id = roomId; + payload.grid_src_room_alias = roomAlias; + this.getDispatcher().dispatch(payload); + }); + const openRoom = { + store: new RoomViewStore(dispatcher), + dispatcher, + dispatcherRef, + }; + + dispatcher.dispatch({ + action: 'view_room', + room_id: roomId, + room_alias: roomAlias, + }, true); + + return openRoom; + } + + _setSingleOpenRoom(payload) { + this._setState({ + rooms: [this._createOpenRoom(payload.room_id, payload.room_alias)], + currentIndex: 0, + }); + } + + _setGroupOpenRooms(groupId) { + this._cleanupOpenRooms(); + // TODO: register to GroupStore updates + const rooms = GroupStore.getGroupRooms(groupId); + const openRooms = rooms.map((room) => { + return this._createOpenRoom(room.roomId); + }); + this._setState({ + rooms: openRooms, + group_id: groupId, + currentIndex: 0, + }); + } + + _forwardAction(payload) { + // 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); + } + } + + async _resolveRoomAlias(payload) { + try { + const result = await MatrixClientPeg.get() + .getRoomIdForAlias(payload.room_alias); + this.getDispatcher().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, + }); + } + } + + _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) { + let proposedIndex; + 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': + this._viewRoom(payload); + break; + case 'view_my_groups': + case 'view_group': + this._forwardAction(payload); + this._cleanupOpenRooms(); + 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': + case 'focus_composer': + this._forwardAction(payload); + break; + case 'forward_event': + this._forwardingEvent = payload.event; + break; + case 'group_grid_set_active': + proposedIndex = this._roomIndex(payload); + if (proposedIndex !== -1) { + this._setState({ + currentIndex: proposedIndex, + }); + } + break; + case 'group_grid_view': + if (payload.group_id !== this._state.group_id) { + this._setGroupOpenRooms(payload.group_id); + } + 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 9e048e5d8e..a0b831ad17 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'; @@ -53,12 +52,12 @@ const INITIAL_STATE = { * with a subset of the js-sdk. * ``` */ -class RoomViewStore extends Store { - constructor() { - super(dis); +export class RoomViewStore extends Store { + constructor(dispatcher) { + super(dispatcher); // Initialise state - this._state = INITIAL_STATE; + this._state = Object.assign({}, INITIAL_STATE); } _setState(newState) { @@ -85,6 +84,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 +151,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 +165,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, - }); - }); } } @@ -219,7 +190,7 @@ 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, }); @@ -335,8 +306,7 @@ class RoomViewStore extends Store { } } -let singletonRoomViewStore = null; -if (!singletonRoomViewStore) { - singletonRoomViewStore = new RoomViewStore(); -} -module.exports = singletonRoomViewStore; +const MatrixDispatcher = require("../matrix-dispatcher"); +const backwardsCompatInstance = new RoomViewStore(new MatrixDispatcher()); + +export default backwardsCompatInstance; diff --git a/src/utils/Timer.js b/src/utils/Timer.js index aeac0887c9..ca06237fbf 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); @@ -70,6 +69,7 @@ export default class Timer { /** * if not started before, starts the timer. + * @returns {Timer} the same timer */ start() { if (!this.isRunning()) { @@ -81,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()) { @@ -98,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()) {