diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index d52599abe9..9df4630136 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -38,6 +38,7 @@ import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks"; import {Group} from "matrix-js-sdk"; import {allSettled, sleep} from "../../utils/promise"; +import RightPanelStore from "../../stores/RightPanelStore"; const LONG_DESC_PLACEHOLDER = _td( `

HTML for your community's page

@@ -542,10 +543,6 @@ export default createReactClass({ }); }, - _onShowRhsClick: function(ev) { - dis.dispatch({ action: 'show_right_panel' }); - }, - _onEditClick: function() { this.setState({ editing: true, @@ -583,6 +580,10 @@ export default createReactClass({ profileForm: null, }); break; + case 'after_right_panel_phase_change': + // We don't keep state on the right panel, so just re-render to update + this.forceUpdate(); + break; default: break; } @@ -1298,7 +1299,9 @@ export default createReactClass({ ); } - const rightPanel = !this.props.collapsedRhs ? : undefined; + const rightPanel = !RightPanelStore.getSharedInstance().isOpenForGroup + ? + : undefined; const headerClasses = { "mx_GroupView_header": true, @@ -1326,9 +1329,9 @@ export default createReactClass({
{ rightButtons }
- + - + { this._getMembershipSection() } { this._getGroupSection() } diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 9b29c56b16..df2eebd7c9 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -70,7 +70,6 @@ const LoggedInView = createReactClass({ // Called with the credentials of a registered user (if they were a ROU that // transitioned to PWLU) onRegistered: PropTypes.func, - collapsedRhs: PropTypes.bool, // Used by the RoomView to handle joining rooms viaServers: PropTypes.arrayOf(PropTypes.string), @@ -554,7 +553,6 @@ const LoggedInView = createReactClass({ eventPixelOffset={this.props.initialEventPixelOffset} key={this.props.currentRoomId || 'roomview'} disabled={this.props.middleDisabled} - collapsedRhs={this.props.collapsedRhs} ConferenceHandler={this.props.ConferenceHandler} resizeNotifier={this.props.resizeNotifier} />; @@ -585,7 +583,6 @@ const LoggedInView = createReactClass({ pageElement = ; break; } diff --git a/src/components/structures/MainSplit.js b/src/components/structures/MainSplit.js index 163755ff1a..bd7bfd8780 100644 --- a/src/components/structures/MainSplit.js +++ b/src/components/structures/MainSplit.js @@ -62,7 +62,7 @@ export default class MainSplit extends React.Component { } componentDidMount() { - if (this.props.panel && !this.props.collapsedRhs) { + if (this.props.panel) { this._createResizer(); } } @@ -74,23 +74,6 @@ export default class MainSplit extends React.Component { } } - componentDidUpdate(prevProps) { - const wasExpanded = !this.props.collapsedRhs && prevProps.collapsedRhs; - const wasCollapsed = this.props.collapsedRhs && !prevProps.collapsedRhs; - const wasPanelSet = this.props.panel && !prevProps.panel; - const wasPanelCleared = !this.props.panel && prevProps.panel; - - if (this.resizeContainer && (wasExpanded || wasPanelSet)) { - // The resizer can only be created when **both** expanded and the panel is - // set. Once both are true, the container ref will mount, which is required - // for the resizer to work. - this._createResizer(); - } else if (this.resizer && (wasCollapsed || wasPanelCleared)) { - this.resizer.detach(); - this.resizer = null; - } - } - render() { const bodyView = React.Children.only(this.props.children); const panelView = this.props.panel; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 1ea7243303..8105805ab0 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -177,10 +177,9 @@ export default createReactClass({ viewUserId: null, // this is persisted as mx_lhs_size, loaded in LoggedInView collapseLhs: false, - collapsedRhs: window.localStorage.getItem("mx_rhs_collapsed") === "true", leftDisabled: false, middleDisabled: false, - rightDisabled: false, + // the right panel's disabled state is tracked in its store. version: null, newVersion: null, @@ -659,23 +658,11 @@ export default createReactClass({ collapseLhs: false, }); break; - case 'hide_right_panel': - window.localStorage.setItem("mx_rhs_collapsed", true); - this.setState({ - collapsedRhs: true, - }); - break; - case 'show_right_panel': - window.localStorage.setItem("mx_rhs_collapsed", false); - this.setState({ - collapsedRhs: false, - }); - break; case 'panel_disable': { this.setState({ leftDisabled: payload.leftDisabled || payload.sideDisabled || false, middleDisabled: payload.middleDisabled || false, - rightDisabled: payload.rightDisabled || payload.sideDisabled || false, + // We don't track the right panel being disabled here - it's tracked in the store. }); break; } @@ -1247,7 +1234,6 @@ export default createReactClass({ view: VIEWS.LOGIN, ready: false, collapseLhs: false, - collapsedRhs: false, currentRoomId: null, }); this.subTitleStatus = ''; @@ -1263,7 +1249,6 @@ export default createReactClass({ view: VIEWS.SOFT_LOGOUT, ready: false, collapseLhs: false, - collapsedRhs: false, currentRoomId: null, }); this.subTitleStatus = ''; @@ -1707,8 +1692,6 @@ export default createReactClass({ handleResize: function(e) { const hideLhsThreshold = 1000; const showLhsThreshold = 1000; - const hideRhsThreshold = 820; - const showRhsThreshold = 820; if (this._windowWidth > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) { dis.dispatch({ action: 'hide_left_panel' }); @@ -1716,12 +1699,6 @@ export default createReactClass({ if (this._windowWidth <= showLhsThreshold && window.innerWidth > showLhsThreshold) { dis.dispatch({ action: 'show_left_panel' }); } - if (this._windowWidth > hideRhsThreshold && window.innerWidth <= hideRhsThreshold) { - dis.dispatch({ action: 'hide_right_panel' }); - } - if (this._windowWidth <= showRhsThreshold && window.innerWidth > showRhsThreshold) { - dis.dispatch({ action: 'show_right_panel' }); - } this.state.resizeNotifier.notifyWindowResized(); this._windowWidth = window.innerWidth; diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 895f6ae57e..1745c9d7dc 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -1,9 +1,9 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd -Copyright 2017 New Vector Ltd -Copyright 2018 New Vector Ltd +Copyright 2017, 2018 New Vector Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -28,13 +28,15 @@ import RateLimitedFunc from '../../ratelimitedfunc'; import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker'; import GroupStore from '../../stores/GroupStore'; import SettingsStore from "../../settings/SettingsStore"; +import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases"; +import RightPanelStore from "../../stores/RightPanelStore"; export default class RightPanel extends React.Component { static get propTypes() { return { roomId: PropTypes.string, // if showing panels for a given room, this is set groupId: PropTypes.string, // if showing panels for a given group, this is set - user: PropTypes.object, + user: PropTypes.object, // used if we know the user ahead of opening the panel }; } @@ -44,23 +46,12 @@ export default class RightPanel extends React.Component { }; } - static Phase = Object.freeze({ - RoomMemberList: 'RoomMemberList', - GroupMemberList: 'GroupMemberList', - GroupRoomList: 'GroupRoomList', - GroupRoomInfo: 'GroupRoomInfo', - FilePanel: 'FilePanel', - NotificationPanel: 'NotificationPanel', - RoomMemberInfo: 'RoomMemberInfo', - Room3pidMemberInfo: 'Room3pidMemberInfo', - GroupMemberInfo: 'GroupMemberInfo', - }); - constructor(props, context) { super(props, context); this.state = { phase: this._getPhaseFromProps(), isUserPrivilegedInGroup: null, + member: this._getUserForPanel(), }; this.onAction = this.onAction.bind(this); this.onRoomStateMember = this.onRoomStateMember.bind(this); @@ -73,13 +64,30 @@ export default class RightPanel extends React.Component { }, 500); } + // Helper function to split out the logic for _getPhaseFromProps() and the constructor + // as both are called at the same time in the constructor. + _getUserForPanel() { + if (this.state && this.state.member) return this.state.member; + const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams; + return this.props.user || lastParams['member']; + } + _getPhaseFromProps() { + const rps = RightPanelStore.getSharedInstance(); if (this.props.groupId) { - return RightPanel.Phase.GroupMemberList; - } else if (this.props.user) { - return RightPanel.Phase.RoomMemberInfo; + if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) { + dis.dispatch({action: "set_right_panel_phase", phase: RIGHT_PANEL_PHASES.GroupMemberList}); + return RIGHT_PANEL_PHASES.GroupMemberList; + } + return rps.groupPanelPhase; + } else if (this._getUserForPanel()) { + return RIGHT_PANEL_PHASES.RoomMemberInfo; } else { - return RightPanel.Phase.RoomMemberList; + if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.roomPanelPhase)) { + dis.dispatch({action: "set_right_panel_phase", phase: RIGHT_PANEL_PHASES.RoomMemberList}); + return RIGHT_PANEL_PHASES.RoomMemberList; + } + return rps.roomPanelPhase; } } @@ -88,9 +96,6 @@ export default class RightPanel extends React.Component { const cli = this.context.matrixClient; cli.on("RoomState.members", this.onRoomStateMember); this._initGroupStore(this.props.groupId); - if (this.props.user) { - this.setState({member: this.props.user}); - } } componentWillUnmount() { @@ -126,7 +131,7 @@ export default class RightPanel extends React.Component { onInviteToGroupButtonClick() { showGroupInviteDialog(this.props.groupId).then(() => { this.setState({ - phase: RightPanel.Phase.GroupMemberList, + phase: RIGHT_PANEL_PHASES.GroupMemberList, }); }); } @@ -142,9 +147,9 @@ export default class RightPanel extends React.Component { return; } // redraw the badge on the membership list - if (this.state.phase === RightPanel.Phase.RoomMemberList && member.roomId === this.props.roomId) { + if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberList && member.roomId === this.props.roomId) { this._delayedUpdate(); - } else if (this.state.phase === RightPanel.Phase.RoomMemberInfo && member.roomId === this.props.roomId && + } else if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo && member.roomId === this.props.roomId && member.userId === this.state.member.userId) { // refresh the member info (e.g. new power level) this._delayedUpdate(); @@ -152,7 +157,7 @@ export default class RightPanel extends React.Component { } onAction(payload) { - if (payload.action === "view_right_panel_phase") { + if (payload.action === "after_right_panel_phase_change") { this.setState({ phase: payload.phase, groupRoomId: payload.groupRoomId, @@ -178,13 +183,13 @@ export default class RightPanel extends React.Component { let panel =
; - if (this.props.roomId && this.state.phase === RightPanel.Phase.RoomMemberList) { + if (this.props.roomId && this.state.phase === RIGHT_PANEL_PHASES.RoomMemberList) { panel = ; - } else if (this.props.groupId && this.state.phase === RightPanel.Phase.GroupMemberList) { + } else if (this.props.groupId && this.state.phase === RIGHT_PANEL_PHASES.GroupMemberList) { panel = ; - } else if (this.state.phase === RightPanel.Phase.GroupRoomList) { + } else if (this.state.phase === RIGHT_PANEL_PHASES.GroupRoomList) { panel = ; - } else if (this.state.phase === RightPanel.Phase.RoomMemberInfo) { + } else if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo) { if (SettingsStore.isFeatureEnabled("feature_dm_verification")) { const onClose = () => { dis.dispatch({ @@ -201,9 +206,9 @@ export default class RightPanel extends React.Component { } else { panel = ; } - } else if (this.state.phase === RightPanel.Phase.Room3pidMemberInfo) { + } else if (this.state.phase === RIGHT_PANEL_PHASES.Room3pidMemberInfo) { panel = ; - } else if (this.state.phase === RightPanel.Phase.GroupMemberInfo) { + } else if (this.state.phase === RIGHT_PANEL_PHASES.GroupMemberInfo) { if (SettingsStore.isFeatureEnabled("feature_dm_verification")) { const onClose = () => { dis.dispatch({ @@ -225,14 +230,14 @@ export default class RightPanel extends React.Component { /> ); } - } else if (this.state.phase === RightPanel.Phase.GroupRoomInfo) { + } else if (this.state.phase === RIGHT_PANEL_PHASES.GroupRoomInfo) { panel = ; - } else if (this.state.phase === RightPanel.Phase.NotificationPanel) { + } else if (this.state.phase === RIGHT_PANEL_PHASES.NotificationPanel) { panel = ; - } else if (this.state.phase === RightPanel.Phase.FilePanel) { + } else if (this.state.phase === RIGHT_PANEL_PHASES.FilePanel) { panel = ; } diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index c8863773f4..4823b0976c 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -108,20 +108,9 @@ module.exports = createReactClass({ ), }); }); - - // dis.dispatch({ - // action: 'panel_disable', - // sideDisabled: true, - // middleDisabled: true, - // }); }, componentWillUnmount: function() { - // dis.dispatch({ - // action: 'panel_disable', - // sideDisabled: false, - // middleDisabled: false, - // }); if (this.filterTimeout) { clearTimeout(this.filterTimeout); } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index b2c39a1225..d78c9923c2 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -54,6 +54,7 @@ import WidgetEchoStore from '../../stores/WidgetEchoStore'; import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import WidgetUtils from '../../utils/WidgetUtils'; import AccessibleButton from "../views/elements/AccessibleButton"; +import RightPanelStore from "../../stores/RightPanelStore"; const DEBUG = false; let debuglog = function() {}; @@ -98,9 +99,6 @@ module.exports = createReactClass({ // * invited us to the room oobData: PropTypes.object, - // is the RightPanel collapsed? - collapsedRhs: PropTypes.bool, - // Servers the RoomView can use to try and assist joins viaServers: PropTypes.arrayOf(PropTypes.string), }, @@ -587,6 +585,10 @@ module.exports = createReactClass({ onAction: function(payload) { switch (payload.action) { + case 'after_right_panel_phase_change': + // We don't keep state on the right panel, so just re-render to update + this.forceUpdate(); + break; case 'message_send_failed': case 'message_sent': this._checkIfAlone(this.state.room); @@ -1717,7 +1719,7 @@ module.exports = createReactClass({ let aux = null; let previewBar; let hideCancel = false; - let hideRightPanel = false; + let forceHideRightPanel = false; if (this.state.forwardingEvent !== null) { aux = ; } else if (this.state.searching) { @@ -1763,7 +1765,7 @@ module.exports = createReactClass({
); } else { - hideRightPanel = true; + forceHideRightPanel = true; } } else if (hiddenHighlightCount > 0) { aux = ( @@ -1951,9 +1953,11 @@ module.exports = createReactClass({ }, ); - const rightPanel = !hideRightPanel && this.state.room && - ; - const collapsedRhs = hideRightPanel || this.props.collapsedRhs; + const showRightPanel = !forceHideRightPanel && this.state.room + && RightPanelStore.getSharedInstance().isOpenForRoom; + const rightPanel = showRightPanel + ? + : null; return (
@@ -1963,7 +1967,6 @@ module.exports = createReactClass({ searchInfo={searchInfo} oobData={this.props.oobData} inRoom={myMembership === 'join'} - collapsedRhs={collapsedRhs} onSearchClick={this.onSearchClick} onSettingsClick={this.onSettingsClick} onPinnedClick={this.onPinnedClick} @@ -1974,7 +1977,6 @@ module.exports = createReactClass({ />
diff --git a/src/components/views/groups/GroupMemberList.js b/src/components/views/groups/GroupMemberList.js index 433625419d..3228a862ce 100644 --- a/src/components/views/groups/GroupMemberList.js +++ b/src/components/views/groups/GroupMemberList.js @@ -1,6 +1,7 @@ /* Copyright 2017 Vector Creations Ltd. Copyright 2017 New Vector Ltd. +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -24,7 +25,7 @@ import PropTypes from 'prop-types'; import { showGroupInviteDialog } from '../../../GroupAddressPicker'; import AccessibleButton from '../elements/AccessibleButton'; import TintableSvg from '../elements/TintableSvg'; -import RightPanel from '../../structures/RightPanel'; +import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases"; const INITIAL_LOAD_NUM_MEMBERS = 30; @@ -163,8 +164,8 @@ export default createReactClass({ onInviteToGroupButtonClick() { showGroupInviteDialog(this.props.groupId).then(() => { dis.dispatch({ - action: 'view_right_panel_phase', - phase: RightPanel.Phase.GroupMemberList, + action: 'set_right_panel_phase', + phase: RIGHT_PANEL_PHASES.GroupMemberList, groupId: this.props.groupId, }); }); diff --git a/src/components/views/right_panel/GroupHeaderButtons.js b/src/components/views/right_panel/GroupHeaderButtons.js index ec14331ad2..c134a5d237 100644 --- a/src/components/views/right_panel/GroupHeaderButtons.js +++ b/src/components/views/right_panel/GroupHeaderButtons.js @@ -3,6 +3,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2017 New Vector Ltd Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,21 +21,21 @@ limitations under the License. import React from 'react'; import { _t } from '../../../languageHandler'; import HeaderButton from './HeaderButton'; -import HeaderButtons from './HeaderButtons'; -import RightPanel from '../../structures/RightPanel'; +import HeaderButtons, {HEADER_KIND_GROUP} from './HeaderButtons'; +import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases"; const GROUP_PHASES = [ - RightPanel.Phase.GroupMemberInfo, - RightPanel.Phase.GroupMemberList, + RIGHT_PANEL_PHASES.GroupMemberInfo, + RIGHT_PANEL_PHASES.GroupMemberList, ]; const ROOM_PHASES = [ - RightPanel.Phase.GroupRoomList, - RightPanel.Phase.GroupRoomInfo, + RIGHT_PANEL_PHASES.GroupRoomList, + RIGHT_PANEL_PHASES.GroupRoomInfo, ]; export default class GroupHeaderButtons extends HeaderButtons { constructor(props) { - super(props, RightPanel.Phase.GroupMemberList); + super(props, HEADER_KIND_GROUP); this._onMembersClicked = this._onMembersClicked.bind(this); this._onRoomsClicked = this._onRoomsClicked.bind(this); } @@ -44,29 +45,34 @@ export default class GroupHeaderButtons extends HeaderButtons { if (payload.action === "view_user") { if (payload.member) { - this.setPhase(RightPanel.Phase.RoomMemberInfo, {member: payload.member}); + this.setPhase(RIGHT_PANEL_PHASES.RoomMemberInfo, {member: payload.member}); } else { - this.setPhase(RightPanel.Phase.GroupMemberList); + this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList); } } else if (payload.action === "view_group") { - this.setPhase(RightPanel.Phase.GroupMemberList); + this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList); } else if (payload.action === "view_group_room") { - this.setPhase(RightPanel.Phase.GroupRoomInfo, {groupRoomId: payload.groupRoomId, groupId: payload.groupId}); + this.setPhase( + RIGHT_PANEL_PHASES.GroupRoomInfo, + {groupRoomId: payload.groupRoomId, groupId: payload.groupId}, + ); } else if (payload.action === "view_group_room_list") { - this.setPhase(RightPanel.Phase.GroupRoomList); + this.setPhase(RIGHT_PANEL_PHASES.GroupRoomList); } else if (payload.action === "view_group_member_list") { - this.setPhase(RightPanel.Phase.GroupMemberList); + this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList); } else if (payload.action === "view_group_user") { - this.setPhase(RightPanel.Phase.GroupMemberInfo, {member: payload.member}); + this.setPhase(RIGHT_PANEL_PHASES.GroupMemberInfo, {member: payload.member}); } } _onMembersClicked() { - this.togglePhase(RightPanel.Phase.GroupMemberList, GROUP_PHASES); + // This toggles for us, if needed + this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList); } _onRoomsClicked() { - this.togglePhase(RightPanel.Phase.GroupRoomList, ROOM_PHASES); + // This toggles for us, if needed + this.setPhase(RIGHT_PANEL_PHASES.GroupRoomList); } renderButtons() { diff --git a/src/components/views/right_panel/HeaderButtons.js b/src/components/views/right_panel/HeaderButtons.js index a01b511dc8..ebe1f5f915 100644 --- a/src/components/views/right_panel/HeaderButtons.js +++ b/src/components/views/right_panel/HeaderButtons.js @@ -3,6 +3,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2017 New Vector Ltd Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,62 +19,50 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import dis from '../../../dispatcher'; +import RightPanelStore from "../../../stores/RightPanelStore"; + +export const HEADER_KIND_ROOM = "room"; +export const HEADER_KIND_GROUP = "group"; + +const HEADER_KINDS = [HEADER_KIND_GROUP, HEADER_KIND_ROOM]; export default class HeaderButtons extends React.Component { - constructor(props, initialPhase) { + constructor(props, kind) { super(props); + if (!HEADER_KINDS.includes(kind)) throw new Error(`Invalid header kind: ${kind}`); + + const rps = RightPanelStore.getSharedInstance(); this.state = { - phase: props.collapsedRhs ? null : initialPhase, - isUserPrivilegedInGroup: null, + headerKind: kind, + phase: kind === HEADER_KIND_ROOM ? rps.visibleRoomPanelPhase : rps.visibleGroupPanelPhase, }; - this.onAction = this.onAction.bind(this); } componentWillMount() { - this.dispatcherRef = dis.register(this.onAction); + this._storeToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelUpdate.bind(this)); + this._dispatcherRef = dis.register(this.onAction.bind(this)); // used by subclasses } componentWillUnmount() { - dis.unregister(this.dispatcherRef); + if (this._storeToken) this._storeToken.remove(); + if (this._dispatcherRef) dis.unregister(this._dispatcherRef); } - componentDidUpdate(prevProps) { - if (!prevProps.collapsedRhs && this.props.collapsedRhs) { - this.setState({ - phase: null, - }); - } + onAction(payload) { + // Ignore - intended to be overridden by subclasses } setPhase(phase, extras) { - if (this.props.collapsedRhs) { - dis.dispatch({ - action: 'show_right_panel', - }); - } - dis.dispatch(Object.assign({ - action: 'view_right_panel_phase', + dis.dispatch({ + action: 'set_right_panel_phase', phase: phase, - }, extras)); + refireParams: extras, + }); } - togglePhase(phase, validPhases = [phase]) { - if (validPhases.includes(this.state.phase)) { - dis.dispatch({ - action: 'hide_right_panel', - }); - } else { - this.setPhase(phase); - } - } - - isPhase(phases) { - if (this.props.collapsedRhs) { - return false; - } + isPhase(phases: string | string[]) { if (Array.isArray(phases)) { return phases.includes(this.state.phase); } else { @@ -81,22 +70,19 @@ export default class HeaderButtons extends React.Component { } } - onAction(payload) { - if (payload.action === "view_right_panel_phase") { - this.setState({ - phase: payload.phase, - }); + onRightPanelUpdate() { + const rps = RightPanelStore.getSharedInstance(); + if (this.state.headerKind === HEADER_KIND_ROOM) { + this.setState({phase: rps.visibleRoomPanelPhase}); + } else if (this.state.head === HEADER_KIND_GROUP) { + this.setState({phase: rps.visibleGroupPanelPhase}); } } render() { // inline style as this will be swapped around in future commits return
- { this.renderButtons() } + {this.renderButtons()}
; } } - -HeaderButtons.propTypes = { - collapsedRhs: PropTypes.bool, -}; diff --git a/src/components/views/right_panel/RoomHeaderButtons.js b/src/components/views/right_panel/RoomHeaderButtons.js index 950fa30e38..f59159d1d9 100644 --- a/src/components/views/right_panel/RoomHeaderButtons.js +++ b/src/components/views/right_panel/RoomHeaderButtons.js @@ -3,6 +3,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2017 New Vector Ltd Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,18 +21,18 @@ limitations under the License. import React from 'react'; import { _t } from '../../../languageHandler'; import HeaderButton from './HeaderButton'; -import HeaderButtons from './HeaderButtons'; -import RightPanel from '../../structures/RightPanel'; +import HeaderButtons, {HEADER_KIND_ROOM} from './HeaderButtons'; +import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases"; const MEMBER_PHASES = [ - RightPanel.Phase.RoomMemberList, - RightPanel.Phase.RoomMemberInfo, - RightPanel.Phase.Room3pidMemberInfo, + RIGHT_PANEL_PHASES.RoomMemberList, + RIGHT_PANEL_PHASES.RoomMemberInfo, + RIGHT_PANEL_PHASES.Room3pidMemberInfo, ]; export default class RoomHeaderButtons extends HeaderButtons { constructor(props) { - super(props, RightPanel.Phase.RoomMemberList); + super(props, HEADER_KIND_ROOM); this._onMembersClicked = this._onMembersClicked.bind(this); this._onFilesClicked = this._onFilesClicked.bind(this); this._onNotificationsClicked = this._onNotificationsClicked.bind(this); @@ -41,31 +42,32 @@ export default class RoomHeaderButtons extends HeaderButtons { super.onAction(payload); if (payload.action === "view_user") { if (payload.member) { - this.setPhase(RightPanel.Phase.RoomMemberInfo, {member: payload.member}); + this.setPhase(RIGHT_PANEL_PHASES.RoomMemberInfo, {member: payload.member}); } else { - this.setPhase(RightPanel.Phase.RoomMemberList); + this.setPhase(RIGHT_PANEL_PHASES.RoomMemberList); } - } else if (payload.action === "view_room" && !this.props.collapsedRhs) { - this.setPhase(RightPanel.Phase.RoomMemberList); } else if (payload.action === "view_3pid_invite") { if (payload.event) { - this.setPhase(RightPanel.Phase.Room3pidMemberInfo, {event: payload.event}); + this.setPhase(RIGHT_PANEL_PHASES.Room3pidMemberInfo, {event: payload.event}); } else { - this.setPhase(RightPanel.Phase.RoomMemberList); + this.setPhase(RIGHT_PANEL_PHASES.RoomMemberList); } } } _onMembersClicked() { - this.togglePhase(RightPanel.Phase.RoomMemberList, MEMBER_PHASES); + // This toggles for us, if needed + this.setPhase(RIGHT_PANEL_PHASES.RoomMemberList); } _onFilesClicked() { - this.togglePhase(RightPanel.Phase.FilePanel); + // This toggles for us, if needed + this.setPhase(RIGHT_PANEL_PHASES.FilePanel); } _onNotificationsClicked() { - this.togglePhase(RightPanel.Phase.NotificationPanel); + // This toggles for us, if needed + this.setPhase(RIGHT_PANEL_PHASES.NotificationPanel); } renderButtons() { @@ -78,13 +80,13 @@ export default class RoomHeaderButtons extends HeaderButtons { />, , , diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 275ec3fafa..eaf2e733ca 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -39,7 +39,6 @@ module.exports = createReactClass({ room: PropTypes.object, oobData: PropTypes.object, inRoom: PropTypes.bool, - collapsedRhs: PropTypes.bool, onSettingsClick: PropTypes.func, onPinnedClick: PropTypes.func, onSearchClick: PropTypes.func, @@ -308,7 +307,7 @@ module.exports = createReactClass({ { topicElement } { cancelButton } { rightRow } - +
); diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 8a00725718..24f256e706 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -181,8 +181,7 @@ export default class Stickerpicker extends React.Component { case "stickerpicker_close": this.setState({showStickers: false}); break; - case "show_right_panel": - case "hide_right_panel": + case "after_right_panel_phase_change": case "show_left_panel": case "hide_left_panel": this.setState({showStickers: false}); diff --git a/src/settings/Settings.js b/src/settings/Settings.js index b02ab82400..82dd639819 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -1,6 +1,7 @@ /* Copyright 2017 Travis Ralston Copyright 2018, 2019 New Vector Ltd. +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -24,6 +25,7 @@ import { import CustomStatusController from "./controllers/CustomStatusController"; import ThemeController from './controllers/ThemeController'; import ReloadOnChangeController from "./controllers/ReloadOnChangeController"; +import {RIGHT_PANEL_PHASES} from "../stores/RightPanelStorePhases"; // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times const LEVELS_ROOM_SETTINGS = ['device', 'room-device', 'room-account', 'account', 'config']; @@ -463,4 +465,20 @@ export const SETTINGS = { displayName: _td("Show previews/thumbnails for images"), default: true, }, + "showRightPanelInRoom": { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: false, + }, + "showRightPanelInGroup": { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: false, + }, + "lastRightPanelPhaseForRoom": { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: RIGHT_PANEL_PHASES.RoomMemberInfo, + }, + "lastRightPanelPhaseForGroup": { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: RIGHT_PANEL_PHASES.GroupMemberList, + }, }; diff --git a/src/settings/handlers/DeviceSettingsHandler.js b/src/settings/handlers/DeviceSettingsHandler.js index 76c518b97b..ed61e9f3be 100644 --- a/src/settings/handlers/DeviceSettingsHandler.js +++ b/src/settings/handlers/DeviceSettingsHandler.js @@ -1,6 +1,7 @@ /* Copyright 2017 Travis Ralston Copyright 2019 New Vector Ltd. +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -56,6 +57,17 @@ export default class DeviceSettingsHandler extends SettingsHandler { return null; // wrong type or otherwise not set } + // Special case the right panel - see `setValue` for rationale. + if ([ + "showRightPanelInRoom", + "showRightPanelInGroup", + "lastRightPanelPhaseForRoom", + "lastRightPanelPhaseForGroup", + ].includes(settingName)) { + const val = JSON.parse(localStorage.getItem(`mx_${settingName}`) || "{}"); + return val['value']; + } + const settings = this._getSettings() || {}; return settings[settingName]; } @@ -81,6 +93,20 @@ export default class DeviceSettingsHandler extends SettingsHandler { return Promise.resolve(); } + // Special case the right panel because we want to be able to update these all + // concurrently without stomping on one another. We could use async/await, though + // that introduces just enough latency to be annoying. + if ([ + "showRightPanelInRoom", + "showRightPanelInGroup", + "lastRightPanelPhaseForRoom", + "lastRightPanelPhaseForGroup", + ].includes(settingName)) { + localStorage.setItem(`mx_${settingName}`, JSON.stringify({value: newValue})); + this._watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue); + return Promise.resolve(); + } + const settings = this._getSettings() || {}; settings[settingName] = newValue; localStorage.setItem("mx_local_settings", JSON.stringify(settings)); diff --git a/src/stores/RightPanelStore.js b/src/stores/RightPanelStore.js new file mode 100644 index 0000000000..37e7498141 --- /dev/null +++ b/src/stores/RightPanelStore.js @@ -0,0 +1,184 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +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 dis from '../dispatcher'; +import {Store} from 'flux/utils'; +import SettingsStore, {SettingLevel} from "../settings/SettingsStore"; +import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "./RightPanelStorePhases"; + +const INITIAL_STATE = { + // Whether or not to show the right panel at all. We split out rooms and groups + // because they're different flows for the user to follow. + showRoomPanel: SettingsStore.getValue("showRightPanelInRoom"), + showGroupPanel: SettingsStore.getValue("showRightPanelInGroup"), + + // The last phase (screen) the right panel was showing + lastRoomPhase: SettingsStore.getValue("lastRightPanelPhaseForRoom"), + lastGroupPhase: SettingsStore.getValue("lastRightPanelPhaseForGroup"), + + // Extra information about the last phase + lastRoomPhaseParams: {}, +}; + +const GROUP_PHASES = Object.keys(RIGHT_PANEL_PHASES).filter(k => k.startsWith("Group")); + +/** + * A class for tracking the state of the right panel between layouts and + * sessions. + */ +export default class RightPanelStore extends Store { + static _instance; + + _inhibitUpdates = false; + + constructor() { + super(dis); + + // Initialise state + this._state = INITIAL_STATE; + } + + get isOpenForRoom(): boolean { + return this._state.showRoomPanel; + } + + get isOpenForGroup(): boolean { + return this._state.showGroupPanel; + } + + get roomPanelPhase(): string { + return this._state.lastRoomPhase; + } + + get groupPanelPhase(): string { + return this._state.lastGroupPhase; + } + + get visibleRoomPanelPhase(): string { + return this.isOpenForRoom ? this.roomPanelPhase : null; + } + + get visibleGroupPanelPhase(): string { + return this.isOpenForGroup ? this.groupPanelPhase : null; + } + + get roomPanelPhaseParams(): any { + return this._state.lastRoomPhaseParams || {}; + } + + _setState(newState) { + this._state = Object.assign(this._state, newState); + + SettingsStore.setValue( + "showRightPanelInRoom", + null, + SettingLevel.DEVICE, + this._state.showRoomPanel, + ); + SettingsStore.setValue( + "showRightPanelInGroup", + null, + SettingLevel.DEVICE, + this._state.showGroupPanel, + ); + + if (RIGHT_PANEL_PHASES_NO_ARGS.includes(this._state.lastRoomPhase)) { + SettingsStore.setValue( + "lastRightPanelPhaseForRoom", + null, + SettingLevel.DEVICE, + this._state.lastRoomPhase, + ); + } + if (RIGHT_PANEL_PHASES_NO_ARGS.includes(this._state.lastGroupPhase)) { + SettingsStore.setValue( + "lastRightPanelPhaseForGroup", + null, + SettingLevel.DEVICE, + this._state.lastGroupPhase, + ); + } + + this.__emitChange(); + } + + __onDispatch(payload) { + if (payload.action === 'panel_disable') { + this._inhibitUpdates = payload.rightDisabled || payload.sideDisabled || false; + return; + } + + if (payload.action === 'view_room' || payload.action === 'view_group') { + // Reset to the member list if we're viewing member info + const memberInfoPhases = [RIGHT_PANEL_PHASES.RoomMemberInfo, RIGHT_PANEL_PHASES.Room3pidMemberInfo]; + if (memberInfoPhases.includes(this._state.lastRoomPhase)) { + this._setState({lastRoomPhase: RIGHT_PANEL_PHASES.RoomMemberList, lastRoomPhaseParams: {}}); + } + + // Do the same for groups + if (this._state.lastGroupPhase === RIGHT_PANEL_PHASES.GroupMemberInfo) { + this._setState({lastGroupPhase: RIGHT_PANEL_PHASES.GroupMemberList}); + } + } + + if (payload.action !== 'set_right_panel_phase' || this._inhibitUpdates) return; + + const targetPhase = payload.phase; + if (!RIGHT_PANEL_PHASES[targetPhase]) { + console.warn(`Tried to switch right panel to unknown phase: ${targetPhase}`); + return; + } + + if (GROUP_PHASES.includes(targetPhase)) { + if (targetPhase === this._state.lastGroupPhase) { + this._setState({ + showGroupPanel: !this._state.showGroupPanel, + }); + } else { + this._setState({ + lastGroupPhase: targetPhase, + showGroupPanel: true, + }); + } + } else { + if (targetPhase === this._state.lastRoomPhase) { + this._setState({ + showRoomPanel: !this._state.showRoomPanel, + }); + } else { + this._setState({ + lastRoomPhase: targetPhase, + showRoomPanel: true, + lastRoomPhaseParams: payload.refireParams || {}, + }); + } + } + + // Let things like the member info panel actually open to the right member. + dis.dispatch({ + action: 'after_right_panel_phase_change', + phase: targetPhase, + ...(payload.refireParams || {}), + }); + } + + static getSharedInstance(): RightPanelStore { + if (!RightPanelStore._instance) { + RightPanelStore._instance = new RightPanelStore(); + } + return RightPanelStore._instance; + } +} diff --git a/src/stores/RightPanelStorePhases.js b/src/stores/RightPanelStorePhases.js new file mode 100644 index 0000000000..96807ebf5b --- /dev/null +++ b/src/stores/RightPanelStorePhases.js @@ -0,0 +1,41 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +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. +*/ + +// These are in their own file because of circular imports being a problem. +export const RIGHT_PANEL_PHASES = Object.freeze({ + // Room stuff + RoomMemberList: 'RoomMemberList', + FilePanel: 'FilePanel', + NotificationPanel: 'NotificationPanel', + RoomMemberInfo: 'RoomMemberInfo', + Room3pidMemberInfo: 'Room3pidMemberInfo', + + // Group stuff + GroupMemberList: 'GroupMemberList', + GroupRoomList: 'GroupRoomList', + GroupRoomInfo: 'GroupRoomInfo', + GroupMemberInfo: 'GroupMemberInfo', +}); + +// These are the phases that are safe to persist (the ones that don't require additional +// arguments). +export const RIGHT_PANEL_PHASES_NO_ARGS = [ + RIGHT_PANEL_PHASES.NotificationPanel, + RIGHT_PANEL_PHASES.FilePanel, + RIGHT_PANEL_PHASES.RoomMemberList, + RIGHT_PANEL_PHASES.GroupMemberList, + RIGHT_PANEL_PHASES.GroupRoomList, +]; diff --git a/test/end-to-end-tests/src/usecases/invite.js b/test/end-to-end-tests/src/usecases/invite.js index d7e02a38d8..814ecd30a6 100644 --- a/test/end-to-end-tests/src/usecases/invite.js +++ b/test/end-to-end-tests/src/usecases/invite.js @@ -17,6 +17,17 @@ limitations under the License. module.exports = async function invite(session, userId) { session.log.step(`invites "${userId}" to room`); await session.delay(1000); + const memberPanelButton = await session.query(".mx_RightPanel_membersButton"); + try { + await session.query(".mx_RightPanel_headerButton_highlight", 500); + // Right panel is open - toggle it to ensure it's the member list + // Sometimes our tests have this opened to MemberInfo + await memberPanelButton.click(); + await memberPanelButton.click(); + } catch (e) { + // Member list is closed - open it + await memberPanelButton.click(); + } const inviteButton = await session.query(".mx_MemberList_invite"); await inviteButton.click(); const inviteTextArea = await session.query(".mx_AddressPickerDialog textarea"); diff --git a/test/end-to-end-tests/src/usecases/memberlist.js b/test/end-to-end-tests/src/usecases/memberlist.js index f6b07b3500..42601b6610 100644 --- a/test/end-to-end-tests/src/usecases/memberlist.js +++ b/test/end-to-end-tests/src/usecases/memberlist.js @@ -62,6 +62,17 @@ module.exports.verifyDeviceForUser = async function(session, name, expectedDevic }; async function getMembersInMemberlist(session) { + const memberPanelButton = await session.query(".mx_RightPanel_membersButton"); + try { + await session.query(".mx_RightPanel_headerButton_highlight", 500); + // Right panel is open - toggle it to ensure it's the member list + // Sometimes our tests have this opened to MemberInfo + await memberPanelButton.click(); + await memberPanelButton.click(); + } catch (e) { + // Member list is closed - open it + await memberPanelButton.click(); + } const memberNameElements = await session.queryAll(".mx_MemberList .mx_EntityTile_name"); return Promise.all(memberNameElements.map(async (el) => { return {label: el, displayName: await session.innerText(el)};