Merge pull request #3703 from matrix-org/travis/right-panel-v2

Refactor RightPanel to match expected behaviour
pull/21833/head
Travis Ralston 2019-12-11 10:50:45 -07:00 committed by GitHub
commit 7230d51cca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 434 additions and 194 deletions

View File

@ -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(
`<h1>HTML for your community's page</h1>
@ -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 ? <RightPanel groupId={this.props.groupId} /> : undefined;
const rightPanel = !RightPanelStore.getSharedInstance().isOpenForGroup
? <RightPanel groupId={this.props.groupId} />
: undefined;
const headerClasses = {
"mx_GroupView_header": true,
@ -1326,9 +1329,9 @@ export default createReactClass({
<div className="mx_GroupView_header_rightCol">
{ rightButtons }
</div>
<GroupHeaderButtons collapsedRhs={this.props.collapsedRhs} />
<GroupHeaderButtons />
</div>
<MainSplit collapsedRhs={this.props.collapsedRhs} panel={rightPanel}>
<MainSplit panel={rightPanel}>
<GeminiScrollbarWrapper className="mx_GroupView_body">
{ this._getMembershipSection() }
{ this._getGroupSection() }

View File

@ -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 = <GroupView
groupId={this.props.currentGroupId}
isNew={this.props.currentGroupIsNew}
collapsedRhs={this.props.collapsedRhs}
/>;
break;
}

View File

@ -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;

View File

@ -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;

View File

@ -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 = <div />;
if (this.props.roomId && this.state.phase === RightPanel.Phase.RoomMemberList) {
if (this.props.roomId && this.state.phase === RIGHT_PANEL_PHASES.RoomMemberList) {
panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />;
} else if (this.props.groupId && this.state.phase === RightPanel.Phase.GroupMemberList) {
} else if (this.props.groupId && this.state.phase === RIGHT_PANEL_PHASES.GroupMemberList) {
panel = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />;
} else if (this.state.phase === RightPanel.Phase.GroupRoomList) {
} else if (this.state.phase === RIGHT_PANEL_PHASES.GroupRoomList) {
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
} 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 = <MemberInfo member={this.state.member} key={this.props.roomId || this.state.member.userId} />;
}
} else if (this.state.phase === RightPanel.Phase.Room3pidMemberInfo) {
} else if (this.state.phase === RIGHT_PANEL_PHASES.Room3pidMemberInfo) {
panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
} 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 = <GroupRoomInfo
groupRoomId={this.state.groupRoomId}
groupId={this.props.groupId}
key={this.state.groupRoomId} />;
} else if (this.state.phase === RightPanel.Phase.NotificationPanel) {
} else if (this.state.phase === RIGHT_PANEL_PHASES.NotificationPanel) {
panel = <NotificationPanel />;
} else if (this.state.phase === RightPanel.Phase.FilePanel) {
} else if (this.state.phase === RIGHT_PANEL_PHASES.FilePanel) {
panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
}

View File

@ -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);
}

View File

@ -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 = <ForwardMessage onCancelClick={this.onCancelClick} />;
} else if (this.state.searching) {
@ -1763,7 +1765,7 @@ module.exports = createReactClass({
</div>
);
} else {
hideRightPanel = true;
forceHideRightPanel = true;
}
} else if (hiddenHighlightCount > 0) {
aux = (
@ -1951,9 +1953,11 @@ module.exports = createReactClass({
},
);
const rightPanel = !hideRightPanel && this.state.room &&
<RightPanel roomId={this.state.room.roomId} resizeNotifier={this.props.resizeNotifier} />;
const collapsedRhs = hideRightPanel || this.props.collapsedRhs;
const showRightPanel = !forceHideRightPanel && this.state.room
&& RightPanelStore.getSharedInstance().isOpenForRoom;
const rightPanel = showRightPanel
? <RightPanel roomId={this.state.room.roomId} resizeNotifier={this.props.resizeNotifier} />
: null;
return (
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref={this._roomView}>
@ -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({
/>
<MainSplit
panel={rightPanel}
collapsedRhs={collapsedRhs}
resizeNotifier={this.props.resizeNotifier}
>
<div className={fadableSectionClasses}>

View File

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

View File

@ -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() {

View File

@ -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 <div className="mx_HeaderButtons" role="tablist">
{ this.renderButtons() }
{this.renderButtons()}
</div>;
}
}
HeaderButtons.propTypes = {
collapsedRhs: PropTypes.bool,
};

View File

@ -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 {
/>,
<HeaderButton key="filesButton" name="filesButton"
title={_t('Files')}
isHighlighted={this.isPhase(RightPanel.Phase.FilePanel)}
isHighlighted={this.isPhase(RIGHT_PANEL_PHASES.FilePanel)}
onClick={this._onFilesClicked}
analytics={['Right Panel', 'File List Button', 'click']}
/>,
<HeaderButton key="notifsButton" name="notifsButton"
title={_t('Notifications')}
isHighlighted={this.isPhase(RightPanel.Phase.NotificationPanel)}
isHighlighted={this.isPhase(RIGHT_PANEL_PHASES.NotificationPanel)}
onClick={this._onNotificationsClicked}
analytics={['Right Panel', 'Notification List Button', 'click']}
/>,

View File

@ -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 }
<RoomHeaderButtons collapsedRhs={this.props.collapsedRhs} />
<RoomHeaderButtons />
</div>
</div>
);

View File

@ -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});

View File

@ -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,
},
};

View File

@ -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));

View File

@ -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;
}
}

View File

@ -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,
];

View File

@ -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");

View File

@ -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)};