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..3a1ff165f1 --- /dev/null +++ b/res/css/structures/_GroupGridView.scss @@ -0,0 +1,129 @@ +/* +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; +} + +.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..a843bb7fee 100644 --- a/res/css/structures/_MatrixChat.scss +++ b/res/css/structures/_MatrixChat.scss @@ -80,7 +80,8 @@ limitations under the License. Empirically this stops the MessagePanel's width exploding outwards when gemini is in 'prevented' mode */ - overflow-x: auto; + // disabling this for now as it clips the active room rect on the grid view + // overflow-x: auto; /* To fix https://github.com/vector-im/riot-web/issues/3298 where Safari needed height 100% all the way down to the HomePage. Height does not 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/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/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; @@ -929,7 +928,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 }, }); @@ -994,10 +993,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; } @@ -1021,14 +1020,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; } @@ -1229,7 +1228,7 @@ module.exports = React.createClass({ }, onSettingsClick: function() { - dis.dispatch({ action: 'open_room_settings' }); + this.props.roomViewStore.getDispatcher().dispatch({ action: 'open_room_settings' }); }, onSettingsSaveClick: function() { @@ -1262,31 +1261,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, }); @@ -1294,7 +1293,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"); @@ -1311,7 +1310,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, }); @@ -1337,7 +1336,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', }); }, @@ -1356,7 +1355,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 @@ -1446,7 +1445,7 @@ module.exports = React.createClass({ }, onFullscreenClick: function() { - dis.dispatch({ + this.props.roomViewStore.getDispatcher().dispatch({ action: 'video_fullscreen', fullscreen: true, }, true); @@ -1571,6 +1570,7 @@ module.exports = React.createClass({
@@ -1617,6 +1617,7 @@ module.exports = React.createClass({
@@ -1758,7 +1759,9 @@ module.exports = React.createClass({ if (canSpeak) { messageComposer = : undefined; + const rightPanel = this.state.room && !this.props.isGrid ? + : + undefined; return (
+ + { _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..226adb910f 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,7 +713,7 @@ module.exports = withMatrixClient(React.createClass({ } if (!member || !member.membership || member.membership === 'leave') { - const roomId = member && member.roomId ? member.roomId : RoomViewStore.getRoomId(); + 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 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..4e7b4d3bbf 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) => { @@ -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/dispatcher.js b/src/dispatcher.js index 48c8dc86e9..4dc6e1e37d 100644 --- a/src/dispatcher.js +++ b/src/dispatcher.js @@ -17,42 +17,10 @@ limitations under the License. 'use strict'; -const flux = require("flux"); - -class MatrixDispatcher extends flux.Dispatcher { - /** - * @param {Object|function} payload Required. The payload to dispatch. - * If an Object, must contain at least an 'action' key. - * If a function, must have the signature (dispatch) => {...}. - * @param {boolean=} sync Optional. Pass true to dispatch - * synchronously. This is useful for anything triggering - * an operation that the browser requires user interaction - * for. - */ - dispatch(payload, sync) { - // Allow for asynchronous dispatching by accepting payloads that have the - // type `function (dispatch) {...}` - if (typeof payload === 'function') { - payload((action) => { - this.dispatch(action, sync); - }); - return; - } - - if (sync) { - super.dispatch(payload); - } else { - // Unless the caller explicitly asked for us to dispatch synchronously, - // we always set a timeout to do this: The flux dispatcher complains - // if you dispatch from within a dispatch, so rather than action - // handlers having to worry about not calling anything that might - // then dispatch, we just do dispatches asynchronously. - setTimeout(super.dispatch.bind(this, payload), 0); - } - } -} +import MatrixDispatcher from "./matrix-dispatcher"; if (global.mxDispatcher === undefined) { global.mxDispatcher = new MatrixDispatcher(); } + module.exports = global.mxDispatcher; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ef1b2e9162..2b976cf4d2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1408,5 +1408,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()) {