Merge pull request #5036 from swapnilraj/swapnilraj/right-panel-ts

Convert right_panel to TS
This commit is contained in:
Jorik Schellekens 2020-07-30 13:37:01 +01:00 committed by GitHub
commit b1f8fe40d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 568 additions and 387 deletions

View file

@ -54,6 +54,7 @@ import LeftPanel from "./LeftPanel";
import CallContainer from '../views/voip/CallContainer';
import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
import RoomListStore from "../../stores/room-list/RoomListStore";
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.
@ -472,8 +473,8 @@ class LoggedInView extends React.Component<IProps, IState> {
case Key.PERIOD:
if (ctrlCmdOnly && (this.props.page_type === "room_view" || this.props.page_type === "group_view")) {
dis.dispatch({
action: 'toggle_right_panel',
dis.dispatch<ToggleRightPanelPayload>({
action: Action.ToggleRightPanel,
type: this.props.page_type === "room_view" ? "room" : "group",
});
handled = true;

View file

@ -26,7 +26,7 @@ import dis from '../../dispatcher/dispatcher';
import RateLimitedFunc from '../../ratelimitedfunc';
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GroupStore from '../../stores/GroupStore';
import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases";
import {RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases";
import RightPanelStore from "../../stores/RightPanelStore";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import {Action} from "../../dispatcher/actions";
@ -75,8 +75,8 @@ export default class RightPanel extends React.Component {
const userForPanel = this._getUserForPanel();
if (this.props.groupId) {
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;
dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList});
return RightPanelPhases.GroupMemberList;
}
return rps.groupPanelPhase;
} else if (userForPanel) {
@ -98,11 +98,11 @@ export default class RightPanel extends React.Component {
) {
return rps.roomPanelPhase;
}
return RIGHT_PANEL_PHASES.RoomMemberInfo;
return RightPanelPhases.RoomMemberInfo;
} else {
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;
dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.RoomMemberList});
return RightPanelPhases.RoomMemberList;
}
return rps.roomPanelPhase;
}
@ -149,7 +149,7 @@ export default class RightPanel extends React.Component {
onInviteToGroupButtonClick() {
showGroupInviteDialog(this.props.groupId).then(() => {
this.setState({
phase: RIGHT_PANEL_PHASES.GroupMemberList,
phase: RightPanelPhases.GroupMemberList,
});
});
}
@ -165,9 +165,9 @@ export default class RightPanel extends React.Component {
return;
}
// redraw the badge on the membership list
if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberList && member.roomId === this.props.roomId) {
if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.roomId) {
this._delayedUpdate();
} else if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo && member.roomId === this.props.roomId &&
} else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.roomId &&
member.userId === this.state.member.userId) {
// refresh the member info (e.g. new power level)
this._delayedUpdate();
@ -175,7 +175,7 @@ export default class RightPanel extends React.Component {
}
onAction(payload) {
if (payload.action === "after_right_panel_phase_change") {
if (payload.action === Action.AfterRightPanelPhaseChange) {
this.setState({
phase: payload.phase,
groupRoomId: payload.groupRoomId,
@ -206,7 +206,7 @@ export default class RightPanel extends React.Component {
// or the member list if we were in the member panel... phew.
dis.dispatch({
action: Action.ViewUser,
member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ? this.state.member : null,
member: this.state.phase === RightPanelPhases.EncryptionPanel ? this.state.member : null,
});
}
};
@ -225,21 +225,21 @@ export default class RightPanel extends React.Component {
let panel = <div />;
switch (this.state.phase) {
case RIGHT_PANEL_PHASES.RoomMemberList:
case RightPanelPhases.RoomMemberList:
if (this.props.roomId) {
panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />;
}
break;
case RIGHT_PANEL_PHASES.GroupMemberList:
case RightPanelPhases.GroupMemberList:
if (this.props.groupId) {
panel = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />;
}
break;
case RIGHT_PANEL_PHASES.GroupRoomList:
case RightPanelPhases.GroupRoomList:
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
break;
case RIGHT_PANEL_PHASES.RoomMemberInfo:
case RIGHT_PANEL_PHASES.EncryptionPanel:
case RightPanelPhases.RoomMemberInfo:
case RightPanelPhases.EncryptionPanel:
panel = <UserInfo
user={this.state.member}
roomId={this.props.roomId}
@ -250,26 +250,26 @@ export default class RightPanel extends React.Component {
verificationRequestPromise={this.state.verificationRequestPromise}
/>;
break;
case RIGHT_PANEL_PHASES.Room3pidMemberInfo:
case RightPanelPhases.Room3pidMemberInfo:
panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
break;
case RIGHT_PANEL_PHASES.GroupMemberInfo:
case RightPanelPhases.GroupMemberInfo:
panel = <UserInfo
user={this.state.member}
groupId={this.props.groupId}
key={this.state.member.userId}
onClose={this.onCloseUserInfo} />;
break;
case RIGHT_PANEL_PHASES.GroupRoomInfo:
case RightPanelPhases.GroupRoomInfo:
panel = <GroupRoomInfo
groupRoomId={this.state.groupRoomId}
groupId={this.props.groupId}
key={this.state.groupRoomId} />;
break;
case RIGHT_PANEL_PHASES.NotificationPanel:
case RightPanelPhases.NotificationPanel:
panel = <NotificationPanel />;
break;
case RIGHT_PANEL_PHASES.FilePanel:
case RightPanelPhases.FilePanel:
panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
break;
}

View file

@ -24,8 +24,9 @@ import GroupStore from '../../../stores/GroupStore';
import PropTypes from 'prop-types';
import { showGroupInviteDialog } from '../../../GroupAddressPicker';
import AccessibleButton from '../elements/AccessibleButton';
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import {Action} from "../../../dispatcher/actions";
const INITIAL_LOAD_NUM_MEMBERS = 30;
@ -164,9 +165,9 @@ export default createReactClass({
onInviteToGroupButtonClick() {
showGroupInviteDialog(this.props.groupId).then(() => {
dis.dispatch({
action: 'set_right_panel_phase',
phase: RIGHT_PANEL_PHASES.GroupMemberList,
groupId: this.props.groupId,
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.GroupMemberList,
refireParams: { groupId: this.props.groupId },
});
});
},

View file

@ -22,7 +22,8 @@ import { _t } from '../../../languageHandler';
import {getNameForEventRoom, userLabelForEventRoom}
from '../../../utils/KeyVerificationStateObserver';
import dis from "../../../dispatcher/dispatcher";
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import {Action} from "../../../dispatcher/actions";
export default class MKeyVerificationRequest extends React.Component {
constructor(props) {
@ -48,8 +49,8 @@ export default class MKeyVerificationRequest extends React.Component {
const {verificationRequest} = this.props.mxEvent;
const member = MatrixClientPeg.get().getUser(verificationRequest.otherUserId);
dis.dispatch({
action: "set_right_panel_phase",
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.EncryptionPanel,
refireParams: {verificationRequest, member},
});
};

View file

@ -15,10 +15,10 @@ limitations under the License.
*/
import React from "react";
import PropTypes from "prop-types";
import * as sdk from "../../../index";
import {_t} from "../../../languageHandler";
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
export const PendingActionSpinner = ({text}) => {
const Spinner = sdk.getComponent('elements.Spinner');
@ -28,7 +28,17 @@ export const PendingActionSpinner = ({text}) => {
</div>;
};
const EncryptionInfo = ({
interface IProps {
waitingForOtherParty: boolean;
waitingForNetwork: boolean;
member: RoomMember;
onStartVerification: () => Promise<void>;
isRoomEncrypted: boolean;
inDialog: boolean;
isSelfVerification: boolean;
}
const EncryptionInfo: React.FC<IProps> = ({
waitingForOtherParty,
waitingForNetwork,
member,
@ -36,10 +46,10 @@ const EncryptionInfo = ({
isRoomEncrypted,
inDialog,
isSelfVerification,
}) => {
let content;
}: IProps) => {
let content: JSX.Element;
if (waitingForOtherParty || waitingForNetwork) {
let text;
let text: string;
if (waitingForOtherParty) {
if (isSelfVerification) {
text = _t("Waiting for you to accept on your other session…");
@ -61,7 +71,7 @@ const EncryptionInfo = ({
);
}
let description;
let description: JSX.Element;
if (isRoomEncrypted) {
description = (
<div>
@ -97,10 +107,5 @@ const EncryptionInfo = ({
</div>
</React.Fragment>;
};
EncryptionInfo.propTypes = {
member: PropTypes.object.isRequired,
onStartVerification: PropTypes.func.isRequired,
request: PropTypes.object,
};
export default EncryptionInfo;

View file

@ -15,7 +15,6 @@ limitations under the License.
*/
import React, {useCallback, useEffect, useState} from "react";
import PropTypes from "prop-types";
import EncryptionInfo from "./EncryptionInfo";
import VerificationPanel from "./VerificationPanel";
@ -26,11 +25,23 @@ import Modal from "../../../Modal";
import {PHASE_REQUESTED, PHASE_UNSENT} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import * as sdk from "../../../index";
import {_t} from "../../../languageHandler";
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
// cancellation codes which constitute a key mismatch
const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"];
const EncryptionPanel = (props) => {
interface IProps {
member: RoomMember;
onClose: () => void;
verificationRequest: VerificationRequest;
verificationRequestPromise: Promise<VerificationRequest>;
layout: string;
inDialog: boolean;
isRoomEncrypted: boolean;
}
const EncryptionPanel: React.FC<IProps> = (props: IProps) => {
const {verificationRequest, verificationRequestPromise, member, onClose, layout, isRoomEncrypted} = props;
const [request, setRequest] = useState(verificationRequest);
// state to show a spinner immediately after clicking "start verification",
@ -48,10 +59,10 @@ const EncryptionPanel = (props) => {
useEffect(() => {
async function awaitPromise() {
setRequesting(true);
const request = await verificationRequestPromise;
const requestFromPromise = await verificationRequestPromise;
setRequesting(false);
setRequest(request);
setPhase(request.phase);
setRequest(requestFromPromise);
setPhase(requestFromPromise.phase);
}
if (verificationRequestPromise) {
awaitPromise();
@ -90,7 +101,7 @@ const EncryptionPanel = (props) => {
}
}, [request]);
let cancelButton;
let cancelButton: JSX.Element;
if (layout !== "dialog" && request && request.pending) {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
cancelButton = (<AccessibleButton
@ -104,9 +115,9 @@ const EncryptionPanel = (props) => {
setRequesting(true);
const cli = MatrixClientPeg.get();
const roomId = await ensureDMExists(cli, member.userId);
const verificationRequest = await cli.requestVerificationDM(member.userId, roomId);
setRequest(verificationRequest);
setPhase(verificationRequest.phase);
const verificationRequest_ = await cli.requestVerificationDM(member.userId, roomId);
setRequest(verificationRequest_);
setPhase(verificationRequest_.phase);
}, [member.userId]);
const requested =
@ -144,12 +155,5 @@ const EncryptionPanel = (props) => {
</React.Fragment>);
}
};
EncryptionPanel.propTypes = {
member: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
verificationRequest: PropTypes.object,
layout: PropTypes.string,
inDialog: PropTypes.bool,
};
export default EncryptionPanel;

View file

@ -21,65 +21,68 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import HeaderButton from './HeaderButton';
import HeaderButtons, {HEADER_KIND_GROUP} from './HeaderButtons';
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
import HeaderButtons, {HeaderKind} from './HeaderButtons';
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import {Action} from "../../../dispatcher/actions";
import {ActionPayload} from "../../../dispatcher/payloads";
import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload";
const GROUP_PHASES = [
RIGHT_PANEL_PHASES.GroupMemberInfo,
RIGHT_PANEL_PHASES.GroupMemberList,
RightPanelPhases.GroupMemberInfo,
RightPanelPhases.GroupMemberList,
];
const ROOM_PHASES = [
RIGHT_PANEL_PHASES.GroupRoomList,
RIGHT_PANEL_PHASES.GroupRoomInfo,
RightPanelPhases.GroupRoomList,
RightPanelPhases.GroupRoomInfo,
];
interface IProps {}
export default class GroupHeaderButtons extends HeaderButtons {
constructor(props) {
super(props, HEADER_KIND_GROUP);
this._onMembersClicked = this._onMembersClicked.bind(this);
this._onRoomsClicked = this._onRoomsClicked.bind(this);
constructor(props: IProps) {
super(props, HeaderKind.Group);
this.onMembersClicked = this.onMembersClicked.bind(this);
this.onRoomsClicked = this.onRoomsClicked.bind(this);
}
onAction(payload: ActionPayload) {
protected onAction(payload: ActionPayload) {
super.onAction(payload);
if (payload.action === Action.ViewUser) {
if (payload.member) {
this.setPhase(RIGHT_PANEL_PHASES.RoomMemberInfo, {member: payload.member});
if ((payload as ViewUserPayload).member) {
this.setPhase(RightPanelPhases.RoomMemberInfo, {member: payload.member});
} else {
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList);
this.setPhase(RightPanelPhases.GroupMemberList);
}
} else if (payload.action === "view_group") {
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList);
this.setPhase(RightPanelPhases.GroupMemberList);
} else if (payload.action === "view_group_room") {
this.setPhase(
RIGHT_PANEL_PHASES.GroupRoomInfo,
RightPanelPhases.GroupRoomInfo,
{groupRoomId: payload.groupRoomId, groupId: payload.groupId},
);
} else if (payload.action === "view_group_room_list") {
this.setPhase(RIGHT_PANEL_PHASES.GroupRoomList);
this.setPhase(RightPanelPhases.GroupRoomList);
} else if (payload.action === "view_group_member_list") {
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList);
this.setPhase(RightPanelPhases.GroupMemberList);
} else if (payload.action === "view_group_user") {
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberInfo, {member: payload.member});
this.setPhase(RightPanelPhases.GroupMemberInfo, {member: payload.member});
}
}
_onMembersClicked() {
if (this.state.phase === RIGHT_PANEL_PHASES.GroupMemberInfo) {
private onMembersClicked() {
if (this.state.phase === RightPanelPhases.GroupMemberInfo) {
// send the active phase to trigger a toggle
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberInfo);
this.setPhase(RightPanelPhases.GroupMemberInfo);
} else {
// This toggles for us, if needed
this.setPhase(RIGHT_PANEL_PHASES.GroupMemberList);
this.setPhase(RightPanelPhases.GroupMemberList);
}
}
_onRoomsClicked() {
private onRoomsClicked() {
// This toggles for us, if needed
this.setPhase(RIGHT_PANEL_PHASES.GroupRoomList);
this.setPhase(RightPanelPhases.GroupRoomList);
}
renderButtons() {
@ -87,13 +90,13 @@ export default class GroupHeaderButtons extends HeaderButtons {
<HeaderButton key="groupMembersButton" name="groupMembersButton"
title={_t('Members')}
isHighlighted={this.isPhase(GROUP_PHASES)}
onClick={this._onMembersClicked}
onClick={this.onMembersClicked}
analytics={['Right Panel', 'Group Member List Button', 'click']}
/>,
<HeaderButton key="roomsButton" name="roomsButton"
title={_t('Rooms')}
isHighlighted={this.isPhase(ROOM_PHASES)}
onClick={this._onRoomsClicked}
onClick={this.onRoomsClicked}
analytics={['Right Panel', 'Group Room List Button', 'click']}
/>,
];

View file

@ -19,23 +19,38 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Analytics from '../../../Analytics';
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
export default class HeaderButton extends React.Component {
constructor() {
super();
interface IProps {
// Whether this button is highlighted
isHighlighted: boolean;
// click handler
onClick: () => void;
// The badge to display above the icon
badge?: React.ReactNode;
// The parameters to track the click event
analytics: string[];
// Button name
name: string;
// Button title
title: string;
}
export default class HeaderButton extends React.Component<IProps> {
constructor(props: IProps) {
super(props);
this.onClick = this.onClick.bind(this);
}
onClick(ev) {
private onClick() {
Analytics.trackEvent(...this.props.analytics);
this.props.onClick();
}
render() {
public render() {
const classes = classNames({
mx_RightPanel_headerButton: true,
mx_RightPanel_headerButton_highlight: this.props.isHighlighted,
@ -51,19 +66,3 @@ export default class HeaderButton extends React.Component {
/>;
}
}
HeaderButton.propTypes = {
// Whether this button is highlighted
isHighlighted: PropTypes.bool.isRequired,
// click handler
onClick: PropTypes.func.isRequired,
// The badge to display above the icon
badge: PropTypes.node,
// The parameters to track the click event
analytics: PropTypes.arrayOf(PropTypes.string).isRequired,
// Button name
name: PropTypes.string.isRequired,
// Button title
title: PropTypes.string.isRequired,
};

View file

@ -1,88 +0,0 @@
/*
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.
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 dis from '../../../dispatcher/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, kind) {
super(props);
if (!HEADER_KINDS.includes(kind)) throw new Error(`Invalid header kind: ${kind}`);
const rps = RightPanelStore.getSharedInstance();
this.state = {
headerKind: kind,
phase: kind === HEADER_KIND_ROOM ? rps.visibleRoomPanelPhase : rps.visibleGroupPanelPhase,
};
}
componentDidMount() {
this._storeToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelUpdate.bind(this));
this._dispatcherRef = dis.register(this.onAction.bind(this)); // used by subclasses
}
componentWillUnmount() {
if (this._storeToken) this._storeToken.remove();
if (this._dispatcherRef) dis.unregister(this._dispatcherRef);
}
onAction(payload) {
// Ignore - intended to be overridden by subclasses
}
setPhase(phase, extras) {
dis.dispatch({
action: 'set_right_panel_phase',
phase: phase,
refireParams: extras,
});
}
isPhase(phases: string | string[]) {
if (Array.isArray(phases)) {
return phases.includes(this.state.phase);
} else {
return phases === this.state.phase;
}
}
onRightPanelUpdate() {
const rps = RightPanelStore.getSharedInstance();
if (this.state.headerKind === HEADER_KIND_ROOM) {
this.setState({phase: rps.visibleRoomPanelPhase});
} else if (this.state.headerKind === 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()}
</div>;
}
}

View file

@ -0,0 +1,110 @@
/*
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.
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 dis from '../../../dispatcher/dispatcher';
import RightPanelStore from "../../../stores/RightPanelStore";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import {Action} from '../../../dispatcher/actions';
import {SetRightPanelPhasePayload, SetRightPanelPhaseRefireParams} from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
import {EventSubscription} from "fbemitter";
export enum HeaderKind {
Room = "room",
Group = "group",
}
interface IState {
headerKind: HeaderKind;
phase: RightPanelPhases;
}
interface IProps {}
export default class HeaderButtons extends React.Component<IProps, IState> {
private storeToken: EventSubscription;
private dispatcherRef: string;
constructor(props: IProps, kind: HeaderKind) {
super(props);
const rps = RightPanelStore.getSharedInstance();
this.state = {
headerKind: kind,
phase: kind === HeaderKind.Room ? rps.visibleRoomPanelPhase : rps.visibleGroupPanelPhase,
};
}
public componentDidMount() {
this.storeToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelUpdate.bind(this));
this.dispatcherRef = dis.register(this.onAction.bind(this)); // used by subclasses
}
public componentWillUnmount() {
if (this.storeToken) this.storeToken.remove();
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
}
protected onAction(payload) {
// Ignore - intended to be overridden by subclasses
}
public setPhase(phase: RightPanelPhases, extras?: Partial<SetRightPanelPhaseRefireParams>) {
dis.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: phase,
refireParams: extras,
});
}
public isPhase(phases: string | string[]) {
if (Array.isArray(phases)) {
return phases.includes(this.state.phase);
} else {
return phases === this.state.phase;
}
}
private onRightPanelUpdate() {
const rps = RightPanelStore.getSharedInstance();
if (this.state.headerKind === HeaderKind.Room) {
this.setState({phase: rps.visibleRoomPanelPhase});
} else if (this.state.headerKind === HeaderKind.Group) {
this.setState({phase: rps.visibleGroupPanelPhase});
}
}
// XXX: Make renderButtons a prop
public renderButtons(): JSX.Element[] {
// Ignore - intended to be overridden by subclasses
// Return empty fragment to satisfy the type
return [
<React.Fragment>
</React.Fragment>
];
}
public render() {
// inline style as this will be swapped around in future commits
return <div className="mx_HeaderButtons" role="tablist">
{this.renderButtons()}
</div>;
}
}

View file

@ -21,82 +21,82 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import HeaderButton from './HeaderButton';
import HeaderButtons, {HEADER_KIND_ROOM} from './HeaderButtons';
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
import HeaderButtons, {HeaderKind} from './HeaderButtons';
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import {Action} from "../../../dispatcher/actions";
import {ActionPayload} from "../../../dispatcher/payloads";
const MEMBER_PHASES = [
RIGHT_PANEL_PHASES.RoomMemberList,
RIGHT_PANEL_PHASES.RoomMemberInfo,
RIGHT_PANEL_PHASES.EncryptionPanel,
RIGHT_PANEL_PHASES.Room3pidMemberInfo,
RightPanelPhases.RoomMemberList,
RightPanelPhases.RoomMemberInfo,
RightPanelPhases.EncryptionPanel,
RightPanelPhases.Room3pidMemberInfo,
];
export default class RoomHeaderButtons extends HeaderButtons {
constructor(props) {
super(props, HEADER_KIND_ROOM);
this._onMembersClicked = this._onMembersClicked.bind(this);
this._onFilesClicked = this._onFilesClicked.bind(this);
this._onNotificationsClicked = this._onNotificationsClicked.bind(this);
super(props, HeaderKind.Room);
this.onMembersClicked = this.onMembersClicked.bind(this);
this.onFilesClicked = this.onFilesClicked.bind(this);
this.onNotificationsClicked = this.onNotificationsClicked.bind(this);
}
onAction(payload: ActionPayload) {
protected onAction(payload: ActionPayload) {
super.onAction(payload);
if (payload.action === Action.ViewUser) {
if (payload.member) {
this.setPhase(RIGHT_PANEL_PHASES.RoomMemberInfo, {member: payload.member});
this.setPhase(RightPanelPhases.RoomMemberInfo, {member: payload.member});
} else {
this.setPhase(RIGHT_PANEL_PHASES.RoomMemberList);
this.setPhase(RightPanelPhases.RoomMemberList);
}
} else if (payload.action === "view_3pid_invite") {
if (payload.event) {
this.setPhase(RIGHT_PANEL_PHASES.Room3pidMemberInfo, {event: payload.event});
this.setPhase(RightPanelPhases.Room3pidMemberInfo, {event: payload.event});
} else {
this.setPhase(RIGHT_PANEL_PHASES.RoomMemberList);
this.setPhase(RightPanelPhases.RoomMemberList);
}
}
}
_onMembersClicked() {
if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo) {
private onMembersClicked() {
if (this.state.phase === RightPanelPhases.RoomMemberInfo) {
// send the active phase to trigger a toggle
// XXX: we should pass refireParams here but then it won't collapse as we desire it to
this.setPhase(RIGHT_PANEL_PHASES.RoomMemberInfo);
this.setPhase(RightPanelPhases.RoomMemberInfo);
} else {
// This toggles for us, if needed
this.setPhase(RIGHT_PANEL_PHASES.RoomMemberList);
this.setPhase(RightPanelPhases.RoomMemberList);
}
}
_onFilesClicked() {
private onFilesClicked() {
// This toggles for us, if needed
this.setPhase(RIGHT_PANEL_PHASES.FilePanel);
this.setPhase(RightPanelPhases.FilePanel);
}
_onNotificationsClicked() {
private onNotificationsClicked() {
// This toggles for us, if needed
this.setPhase(RIGHT_PANEL_PHASES.NotificationPanel);
this.setPhase(RightPanelPhases.NotificationPanel);
}
renderButtons() {
public renderButtons() {
return [
<HeaderButton key="membersButton" name="membersButton"
title={_t('Members')}
isHighlighted={this.isPhase(MEMBER_PHASES)}
onClick={this._onMembersClicked}
onClick={this.onMembersClicked}
analytics={['Right Panel', 'Member List Button', 'click']}
/>,
<HeaderButton key="filesButton" name="filesButton"
title={_t('Files')}
isHighlighted={this.isPhase(RIGHT_PANEL_PHASES.FilePanel)}
onClick={this._onFilesClicked}
isHighlighted={this.isPhase(RightPanelPhases.FilePanel)}
onClick={this.onFilesClicked}
analytics={['Right Panel', 'File List Button', 'click']}
/>,
<HeaderButton key="notifsButton" name="notifsButton"
title={_t('Notifications')}
isHighlighted={this.isPhase(RIGHT_PANEL_PHASES.NotificationPanel)}
onClick={this._onNotificationsClicked}
isHighlighted={this.isPhase(RightPanelPhases.NotificationPanel)}
onClick={this.onNotificationsClicked}
analytics={['Right Panel', 'Notification List Button', 'click']}
/>,
];

View file

@ -40,7 +40,7 @@ import E2EIcon from "../rooms/E2EIcon";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import {textualPowerLevel} from '../../../Roles';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import EncryptionPanel from "./EncryptionPanel";
import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
import { verifyUser, legacyVerifyUser, verifyDevice } from '../../../verification';
@ -1480,7 +1480,7 @@ const UserInfoHeader = ({onClose, member, e2eStatus}) => {
</React.Fragment>;
};
const UserInfo = ({user, groupId, roomId, onClose, phase=RIGHT_PANEL_PHASES.RoomMemberInfo, ...props}) => {
const UserInfo = ({user, groupId, roomId, onClose, phase=RightPanelPhases.RoomMemberInfo, ...props}) => {
const cli = useContext(MatrixClientContext);
// Load room if we are given a room id and memoize it
@ -1500,8 +1500,8 @@ const UserInfo = ({user, groupId, roomId, onClose, phase=RIGHT_PANEL_PHASES.Room
let content;
switch (phase) {
case RIGHT_PANEL_PHASES.RoomMemberInfo:
case RIGHT_PANEL_PHASES.GroupMemberInfo:
case RightPanelPhases.RoomMemberInfo:
case RightPanelPhases.GroupMemberInfo:
content = (
<BasicUserInfo
room={room}
@ -1511,7 +1511,7 @@ const UserInfo = ({user, groupId, roomId, onClose, phase=RIGHT_PANEL_PHASES.Room
isRoomEncrypted={isRoomEncrypted} />
);
break;
case RIGHT_PANEL_PHASES.EncryptionPanel:
case RightPanelPhases.EncryptionPanel:
classes.push("mx_UserInfo_smallAvatar");
content = (
<EncryptionPanel {...props} member={member} onClose={onClose} isRoomEncrypted={isRoomEncrypted} />

View file

@ -15,12 +15,15 @@ limitations under the License.
*/
import React from "react";
import PropTypes from "prop-types";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import * as sdk from '../../../index';
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
import {SCAN_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
import {ReciprocateQRCode} from "matrix-js-sdk/src/crypto/verification/QRCode";
import {SAS} from "matrix-js-sdk/src/crypto/verification/SAS";
import VerificationQRCode from "../elements/crypto/VerificationQRCode";
import {_t} from "../../../languageHandler";
@ -36,37 +39,51 @@ import {
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import Spinner from "../elements/Spinner";
export default class VerificationPanel extends React.PureComponent {
static propTypes = {
layout: PropTypes.string,
request: PropTypes.object.isRequired,
member: PropTypes.object.isRequired,
phase: PropTypes.oneOf([
// XXX: Should be defined in matrix-js-sdk
enum VerificationPhase {
PHASE_UNSENT,
PHASE_REQUESTED,
PHASE_READY,
PHASE_DONE,
PHASE_STARTED,
PHASE_CANCELLED,
PHASE_DONE,
]).isRequired,
onClose: PropTypes.func.isRequired,
isRoomEncrypted: PropTypes.bool,
};
constructor(props) {
super(props);
this.state = {};
this._hasVerifier = false;
}
renderQRPhase() {
interface IProps {
layout: string;
request: VerificationRequest;
member: RoomMember;
phase: VerificationPhase;
onClose: () => void;
isRoomEncrypted: boolean;
inDialog: boolean;
key: number;
}
interface IState {
sasEvent?: SAS;
emojiButtonClicked?: boolean;
reciprocateButtonClicked?: boolean;
reciprocateQREvent?: ReciprocateQRCode;
}
export default class VerificationPanel extends React.PureComponent<IProps, IState> {
private hasVerifier: boolean;
constructor(props: IProps) {
super(props);
this.state = {};
this.hasVerifier = false;
}
private renderQRPhase() {
const {member, request} = this.props;
const showSAS = request.otherPartySupportsMethod(verificationMethods.SAS);
const showQR = request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD);
const showSAS: boolean = request.otherPartySupportsMethod(verificationMethods.SAS);
const showQR: boolean = request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD);
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const brand = SdkConfig.get().brand;
const noCommonMethodError = !showSAS && !showQR ?
const noCommonMethodError: JSX.Element = !showSAS && !showQR ?
<p>{_t(
"The session you are trying to verify doesn't support scanning a " +
"QR code or emoji verification, which is what %(brand)s supports. Try " +
@ -77,41 +94,41 @@ export default class VerificationPanel extends React.PureComponent {
if (this.props.layout === 'dialog') {
// HACK: This is a terrible idea.
let qrBlock;
let sasBlock;
let qrBlockDialog: JSX.Element;
let sasBlockDialog: JSX.Element;
if (showQR) {
qrBlock =
qrBlockDialog =
<div className='mx_VerificationPanel_QRPhase_startOption'>
<p>{_t("Scan this unique code")}</p>
<VerificationQRCode qrCodeData={request.qrCodeData} />
</div>;
}
if (showSAS) {
sasBlock =
sasBlockDialog =
<div className='mx_VerificationPanel_QRPhase_startOption'>
<p>{_t("Compare unique emoji")}</p>
<span className='mx_VerificationPanel_QRPhase_helpText'>{_t("Compare a unique set of emoji if you don't have a camera on either device")}</span>
<AccessibleButton disabled={this.state.emojiButtonClicked} onClick={this._startSAS} kind='primary'>
<AccessibleButton disabled={this.state.emojiButtonClicked} onClick={this.startSAS} kind='primary'>
{_t("Start")}
</AccessibleButton>
</div>;
}
const or = qrBlock && sasBlock ?
const or = qrBlockDialog && sasBlockDialog ?
<div className='mx_VerificationPanel_QRPhase_betweenText'>{_t("or")}</div> : null;
return (
<div>
{_t("Verify this session by completing one of the following:")}
<div className='mx_VerificationPanel_QRPhase_startOptions'>
{qrBlock}
{qrBlockDialog}
{or}
{sasBlock}
{sasBlockDialog}
{noCommonMethodError}
</div>
</div>
);
}
let qrBlock;
let qrBlock: JSX.Element;
if (showQR) {
qrBlock = <div className="mx_UserInfo_container">
<h3>{_t("Verify by scanning")}</h3>
@ -125,7 +142,7 @@ export default class VerificationPanel extends React.PureComponent {
</div>;
}
let sasBlock;
let sasBlock: JSX.Element;
if (showSAS) {
const disabled = this.state.emojiButtonClicked;
const sasLabel = showQR ?
@ -140,7 +157,7 @@ export default class VerificationPanel extends React.PureComponent {
disabled={disabled}
kind="primary"
className="mx_UserInfo_wideButton mx_VerificationPanel_verifyByEmojiButton"
onClick={this._startSAS}
onClick={this.startSAS}
>
{_t("Verify by emoji")}
</AccessibleButton>
@ -159,22 +176,22 @@ export default class VerificationPanel extends React.PureComponent {
</React.Fragment>;
}
_onReciprocateYesClick = () => {
private onReciprocateYesClick = () => {
this.setState({reciprocateButtonClicked: true});
this.state.reciprocateQREvent.confirm();
};
_onReciprocateNoClick = () => {
private onReciprocateNoClick = () => {
this.setState({reciprocateButtonClicked: true});
this.state.reciprocateQREvent.cancel();
};
_getDevice() {
private getDevice() {
const deviceId = this.props.request && this.props.request.channel.deviceId;
return MatrixClientPeg.get().getStoredDevice(MatrixClientPeg.get().getUserId(), deviceId);
}
renderQRReciprocatePhase() {
private renderQRReciprocatePhase() {
const {member, request} = this.props;
let Button;
// a bit of a hack, but the FormButton should only be used in the right panel
@ -189,7 +206,7 @@ export default class VerificationPanel extends React.PureComponent {
_t("Almost there! Is %(displayName)s showing the same shield?", {
displayName: member.displayName || member.name || member.userId,
});
let body;
let body: JSX.Element;
if (this.state.reciprocateQREvent) {
// riot web doesn't support scanning yet, so assume here we're the client being scanned.
//
@ -202,11 +219,11 @@ export default class VerificationPanel extends React.PureComponent {
<Button
label={_t("No")} kind="danger"
disabled={this.state.reciprocateButtonClicked}
onClick={this._onReciprocateNoClick}>{_t("No")}</Button>
onClick={this.onReciprocateNoClick}>{_t("No")}</Button>
<Button
label={_t("Yes")} kind="primary"
disabled={this.state.reciprocateButtonClicked}
onClick={this._onReciprocateYesClick}>{_t("Yes")}</Button>
onClick={this.onReciprocateYesClick}>{_t("Yes")}</Button>
</div>
</React.Fragment>;
} else {
@ -218,10 +235,10 @@ export default class VerificationPanel extends React.PureComponent {
</div>;
}
renderVerifiedPhase() {
private renderVerifiedPhase() {
const {member, request} = this.props;
let text;
let text: string;
if (!request.isSelfVerification) {
if (this.props.isRoomEncrypted) {
text = _t("Verify all users in a room to ensure it's secure.");
@ -230,9 +247,9 @@ export default class VerificationPanel extends React.PureComponent {
}
}
let description;
let description: string;
if (request.isSelfVerification) {
const device = this._getDevice();
const device = this.getDevice();
if (!device) {
// This can happen if the device is logged out while we're still showing verification
// UI for it.
@ -264,19 +281,19 @@ export default class VerificationPanel extends React.PureComponent {
);
}
renderCancelledPhase() {
private renderCancelledPhase() {
const {member, request} = this.props;
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let startAgainInstruction;
let startAgainInstruction: string;
if (request.isSelfVerification) {
startAgainInstruction = _t("Start verification again from the notification.");
} else {
startAgainInstruction = _t("Start verification again from their profile.");
}
let text;
let text: string;
if (request.cancellationCode === "m.timeout") {
text = _t("Verification timed out.") + ` ${startAgainInstruction}`;
} else if (request.cancellingUserId === request.otherUserId) {
@ -304,7 +321,7 @@ export default class VerificationPanel extends React.PureComponent {
);
}
render() {
public render() {
const {member, phase, request} = this.props;
const displayName = member.displayName || member.name || member.userId;
@ -321,10 +338,10 @@ export default class VerificationPanel extends React.PureComponent {
const emojis = this.state.sasEvent ?
<VerificationShowSas
displayName={displayName}
device={this._getDevice()}
device={this.getDevice()}
sas={this.state.sasEvent.sas}
onCancel={this._onSasMismatchesClick}
onDone={this._onSasMatchesClick}
onCancel={this.onSasMismatchesClick}
onDone={this.onSasMatchesClick}
inDialog={this.props.inDialog}
isSelf={request.isSelfVerification}
/> : <Spinner />;
@ -345,7 +362,7 @@ export default class VerificationPanel extends React.PureComponent {
return null;
}
_startSAS = async () => {
private startSAS = async () => {
this.setState({emojiButtonClicked: true});
const verifier = this.props.request.beginKeyVerification(verificationMethods.SAS);
try {
@ -355,31 +372,31 @@ export default class VerificationPanel extends React.PureComponent {
}
};
_onSasMatchesClick = () => {
private onSasMatchesClick = () => {
this.state.sasEvent.confirm();
};
_onSasMismatchesClick = () => {
private onSasMismatchesClick = () => {
this.state.sasEvent.mismatch();
};
_updateVerifierState = () => {
private updateVerifierState = () => {
const {request} = this.props;
const {sasEvent, reciprocateQREvent} = request.verifier;
request.verifier.off('show_sas', this._updateVerifierState);
request.verifier.off('show_reciprocate_qr', this._updateVerifierState);
request.verifier.off('show_sas', this.updateVerifierState);
request.verifier.off('show_reciprocate_qr', this.updateVerifierState);
this.setState({sasEvent, reciprocateQREvent});
};
_onRequestChange = async () => {
private onRequestChange = async () => {
const {request} = this.props;
const hadVerifier = this._hasVerifier;
this._hasVerifier = !!request.verifier;
if (!hadVerifier && this._hasVerifier) {
request.verifier.on('show_sas', this._updateVerifierState);
request.verifier.on('show_reciprocate_qr', this._updateVerifierState);
const hadVerifier = this.hasVerifier;
this.hasVerifier = !!request.verifier;
if (!hadVerifier && this.hasVerifier) {
request.verifier.on('show_sas', this.updateVerifierState);
request.verifier.on('show_reciprocate_qr', this.updateVerifierState);
try {
// on the requester side, this is also awaited in _startSAS,
// on the requester side, this is also awaited in startSAS,
// but that's ok as verify should return the same promise.
await request.verifier.verify();
} catch (err) {
@ -388,23 +405,22 @@ export default class VerificationPanel extends React.PureComponent {
}
};
componentDidMount() {
public componentDidMount() {
const {request} = this.props;
request.on("change", this._onRequestChange);
request.on("change", this.onRequestChange);
if (request.verifier) {
const {request} = this.props;
const {sasEvent, reciprocateQREvent} = request.verifier;
this.setState({sasEvent, reciprocateQREvent});
}
this._onRequestChange();
this.onRequestChange();
}
componentWillUnmount() {
public componentWillUnmount() {
const {request} = this.props;
if (request.verifier) {
request.verifier.off('show_sas', this._updateVerifierState);
request.verifier.off('show_reciprocate_qr', this._updateVerifierState);
request.verifier.off('show_sas', this.updateVerifierState);
request.verifier.off('show_reciprocate_qr', this.updateVerifierState);
}
request.off("change", this._onRequestChange);
request.off("change", this.onRequestChange);
}
}

View file

@ -28,6 +28,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import {ContextMenu} from "../../structures/ContextMenu";
import {WidgetType} from "../../../widgets/WidgetType";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import {Action} from "../../../dispatcher/actions";
// This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
// We sit in a context menu, so this should be given to the context menu.
@ -181,7 +182,7 @@ export default class Stickerpicker extends React.Component {
case "stickerpicker_close":
this.setState({showStickers: false});
break;
case "after_right_panel_phase_change":
case Action.AfterRightPanelPhaseChange:
case "show_left_panel":
case "hide_left_panel":
this.setState({showStickers: false});

View file

@ -19,7 +19,8 @@ import React from "react";
import * as sdk from "../../../index";
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload"
import {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver";
import dis from "../../../dispatcher/dispatcher";
import ToastStore from "../../../stores/ToastStore";
@ -27,6 +28,7 @@ import Modal from "../../../Modal";
import GenericToast from "./GenericToast";
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import {DeviceInfo} from "matrix-js-sdk/src/crypto/deviceinfo";
import {Action} from "../../../dispatcher/actions";
interface IProps {
toastKey: string;
@ -104,9 +106,9 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
room_id: request.channel.roomId,
should_peek: false,
});
dis.dispatch({
action: "set_right_panel_phase",
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
dis.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.EncryptionPanel,
refireParams: {
verificationRequest: request,
member: cli.getUser(request.otherUserId),

View file

@ -79,4 +79,19 @@ export enum Action {
* Changes room based on room list order and payload parameters. Should be used with ViewRoomDeltaPayload.
*/
ViewRoomDelta = "view_room_delta",
/**
* Sets the phase for the right panel. Should be used with SetRightPanelPhasePayload.
*/
SetRightPanelPhase = "set_right_panel_phase",
/**
* Toggles the right panel. Should be used with ToggleRightPanelPayload.
*/
ToggleRightPanel = "toggle_right_panel",
/**
* Trigged after the phase of the right panel is set. Should be used with AfterRightPanelPhaseChangePayload.
*/
AfterRightPanelPhaseChange = "after_right_panel_phase_change",
}

View file

@ -0,0 +1,30 @@
/*
Copyright 2020 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 { RightPanelPhases } from "../../stores/RightPanelStorePhases";
import { SetRightPanelPhaseRefireParams } from "./SetRightPanelPhasePayload";
import { ActionPayload } from "../payloads";
import { Action } from "../actions";
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
interface AfterRightPanelPhaseChangeAction extends ActionPayload {
action: Action.AfterRightPanelPhaseChange;
phase: RightPanelPhases;
verificationRequestPromise?: Promise<VerificationRequest>;
}
export type AfterRightPanelPhaseChangePayload
= AfterRightPanelPhaseChangeAction & SetRightPanelPhaseRefireParams;

View file

@ -0,0 +1,37 @@
/*
Copyright 2020 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 { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
import { ActionPayload } from "../payloads";
import { Action } from "../actions";
export interface SetRightPanelPhasePayload extends ActionPayload {
action: Action.SetRightPanelPhase;
phase: RightPanelPhases;
refireParams?: SetRightPanelPhaseRefireParams;
}
export interface SetRightPanelPhaseRefireParams {
member?: RoomMember;
verificationRequest?: VerificationRequest;
groupId?: string;
groupRoomId?: string;
// XXX: The type for event should 'view_3pid_invite' action's payload
event?: any;
}

View file

@ -0,0 +1,27 @@
/*
Copyright 2020 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 { ActionPayload } from "../payloads";
import { Action } from "../actions";
export interface ToggleRightPanelPayload extends ActionPayload {
action: Action.ToggleRightPanel;
/**
* The type of room that the panel is toggled in.
*/
type: "group" | "room";
}

View file

@ -28,7 +28,7 @@ import CustomStatusController from "./controllers/CustomStatusController";
import ThemeController from './controllers/ThemeController';
import PushToMatrixClientController from './controllers/PushToMatrixClientController';
import ReloadOnChangeController from "./controllers/ReloadOnChangeController";
import {RIGHT_PANEL_PHASES} from "../stores/RightPanelStorePhases";
import {RightPanelPhases} from "../stores/RightPanelStorePhases";
import FontSizeController from './controllers/FontSizeController';
import SystemFontController from './controllers/SystemFontController';
import UseSystemFontController from './controllers/UseSystemFontController';
@ -534,11 +534,11 @@ export const SETTINGS = {
},
"lastRightPanelPhaseForRoom": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: RIGHT_PANEL_PHASES.RoomMemberInfo,
default: RightPanelPhases.RoomMemberInfo,
},
"lastRightPanelPhaseForGroup": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: RIGHT_PANEL_PHASES.GroupMemberList,
default: RightPanelPhases.GroupMemberList,
},
"enableEventIndexing": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,

View file

@ -18,161 +18,177 @@ import dis from '../dispatcher/dispatcher';
import {pendingVerificationRequestForUser} from '../verification';
import {Store} from 'flux/utils';
import SettingsStore, {SettingLevel} from "../settings/SettingsStore";
import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "./RightPanelStorePhases";
import {RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS} from "./RightPanelStorePhases";
import {ActionPayload} from "../dispatcher/payloads";
import {Action} from '../dispatcher/actions';
const INITIAL_STATE = {
interface RightPanelStoreState {
// 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"),
showRoomPanel: boolean;
showGroupPanel: boolean;
// The last phase (screen) the right panel was showing
lastRoomPhase: SettingsStore.getValue("lastRightPanelPhaseForRoom"),
lastGroupPhase: SettingsStore.getValue("lastRightPanelPhaseForGroup"),
lastRoomPhase: RightPanelPhases;
lastGroupPhase: RightPanelPhases;
// Extra information about the last phase
lastRoomPhaseParams: {[key: string]: any};
}
const INITIAL_STATE: RightPanelStoreState = {
showRoomPanel: SettingsStore.getValue("showRightPanelInRoom"),
showGroupPanel: SettingsStore.getValue("showRightPanelInGroup"),
lastRoomPhase: SettingsStore.getValue("lastRightPanelPhaseForRoom"),
lastGroupPhase: SettingsStore.getValue("lastRightPanelPhaseForGroup"),
lastRoomPhaseParams: {},
};
const GROUP_PHASES = Object.keys(RIGHT_PANEL_PHASES).filter(k => k.startsWith("Group"));
const GROUP_PHASES = [
RightPanelPhases.GroupMemberList,
RightPanelPhases.GroupRoomList,
RightPanelPhases.GroupRoomInfo,
RightPanelPhases.GroupMemberInfo,
];
const MEMBER_INFO_PHASES = [
RIGHT_PANEL_PHASES.RoomMemberInfo,
RIGHT_PANEL_PHASES.Room3pidMemberInfo,
RIGHT_PANEL_PHASES.EncryptionPanel,
RightPanelPhases.RoomMemberInfo,
RightPanelPhases.Room3pidMemberInfo,
RightPanelPhases.EncryptionPanel,
];
/**
* A class for tracking the state of the right panel between layouts and
* sessions.
*/
export default class RightPanelStore extends Store {
static _instance;
export default class RightPanelStore extends Store<ActionPayload> {
private static instance: RightPanelStore;
private state: RightPanelStoreState;
constructor() {
super(dis);
// Initialise state
this._state = INITIAL_STATE;
this.state = INITIAL_STATE;
}
get isOpenForRoom(): boolean {
return this._state.showRoomPanel;
return this.state.showRoomPanel;
}
get isOpenForGroup(): boolean {
return this._state.showGroupPanel;
return this.state.showGroupPanel;
}
get roomPanelPhase(): string {
return this._state.lastRoomPhase;
get roomPanelPhase(): RightPanelPhases {
return this.state.lastRoomPhase;
}
get groupPanelPhase(): string {
return this._state.lastGroupPhase;
get groupPanelPhase(): RightPanelPhases {
return this.state.lastGroupPhase;
}
get visibleRoomPanelPhase(): string {
get visibleRoomPanelPhase(): RightPanelPhases {
return this.isOpenForRoom ? this.roomPanelPhase : null;
}
get visibleGroupPanelPhase(): string {
get visibleGroupPanelPhase(): RightPanelPhases {
return this.isOpenForGroup ? this.groupPanelPhase : null;
}
get roomPanelPhaseParams(): any {
return this._state.lastRoomPhaseParams || {};
return this.state.lastRoomPhaseParams || {};
}
_setState(newState) {
this._state = Object.assign(this._state, newState);
private setState(newState: Partial<RightPanelStoreState>) {
this.state = Object.assign(this.state, newState);
SettingsStore.setValue(
"showRightPanelInRoom",
null,
SettingLevel.DEVICE,
this._state.showRoomPanel,
this.state.showRoomPanel,
);
SettingsStore.setValue(
"showRightPanelInGroup",
null,
SettingLevel.DEVICE,
this._state.showGroupPanel,
this.state.showGroupPanel,
);
if (RIGHT_PANEL_PHASES_NO_ARGS.includes(this._state.lastRoomPhase)) {
if (RIGHT_PANEL_PHASES_NO_ARGS.includes(this.state.lastRoomPhase)) {
SettingsStore.setValue(
"lastRightPanelPhaseForRoom",
null,
SettingLevel.DEVICE,
this._state.lastRoomPhase,
this.state.lastRoomPhase,
);
}
if (RIGHT_PANEL_PHASES_NO_ARGS.includes(this._state.lastGroupPhase)) {
if (RIGHT_PANEL_PHASES_NO_ARGS.includes(this.state.lastGroupPhase)) {
SettingsStore.setValue(
"lastRightPanelPhaseForGroup",
null,
SettingLevel.DEVICE,
this._state.lastGroupPhase,
this.state.lastGroupPhase,
);
}
this.__emitChange();
}
__onDispatch(payload) {
__onDispatch(payload: ActionPayload) {
switch (payload.action) {
case 'view_room':
case 'view_group':
// Reset to the member list if we're viewing member info
if (MEMBER_INFO_PHASES.includes(this._state.lastRoomPhase)) {
this._setState({lastRoomPhase: RIGHT_PANEL_PHASES.RoomMemberList, lastRoomPhaseParams: {}});
if (MEMBER_INFO_PHASES.includes(this.state.lastRoomPhase)) {
this.setState({lastRoomPhase: RightPanelPhases.RoomMemberList, lastRoomPhaseParams: {}});
}
// Do the same for groups
if (this._state.lastGroupPhase === RIGHT_PANEL_PHASES.GroupMemberInfo) {
this._setState({lastGroupPhase: RIGHT_PANEL_PHASES.GroupMemberList});
if (this.state.lastGroupPhase === RightPanelPhases.GroupMemberInfo) {
this.setState({lastGroupPhase: RightPanelPhases.GroupMemberList});
}
break;
case 'set_right_panel_phase': {
case Action.SetRightPanelPhase: {
let targetPhase = payload.phase;
let refireParams = payload.refireParams;
// redirect to EncryptionPanel if there is an ongoing verification request
if (targetPhase === RIGHT_PANEL_PHASES.RoomMemberInfo && payload.refireParams) {
if (targetPhase === RightPanelPhases.RoomMemberInfo && payload.refireParams) {
const {member} = payload.refireParams;
const pendingRequest = pendingVerificationRequestForUser(member);
if (pendingRequest) {
targetPhase = RIGHT_PANEL_PHASES.EncryptionPanel;
targetPhase = RightPanelPhases.EncryptionPanel;
refireParams = {
verificationRequest: pendingRequest,
member,
};
}
}
if (!RIGHT_PANEL_PHASES[targetPhase]) {
if (!RightPanelPhases[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,
if (targetPhase === this.state.lastGroupPhase) {
this.setState({
showGroupPanel: !this.state.showGroupPanel,
});
} else {
this._setState({
this.setState({
lastGroupPhase: targetPhase,
showGroupPanel: true,
});
}
} else {
if (targetPhase === this._state.lastRoomPhase && !refireParams) {
this._setState({
showRoomPanel: !this._state.showRoomPanel,
if (targetPhase === this.state.lastRoomPhase && !refireParams) {
this.setState({
showRoomPanel: !this.state.showRoomPanel,
});
} else {
this._setState({
this.setState({
lastRoomPhase: targetPhase,
showRoomPanel: true,
lastRoomPhaseParams: refireParams || {},
@ -182,27 +198,27 @@ export default class RightPanelStore extends Store {
// Let things like the member info panel actually open to the right member.
dis.dispatch({
action: 'after_right_panel_phase_change',
action: Action.AfterRightPanelPhaseChange,
phase: targetPhase,
...(refireParams || {}),
});
break;
}
case 'toggle_right_panel':
case Action.ToggleRightPanel:
if (payload.type === "room") {
this._setState({ showRoomPanel: !this._state.showRoomPanel });
this.setState({ showRoomPanel: !this.state.showRoomPanel });
} else { // group
this._setState({ showGroupPanel: !this._state.showGroupPanel });
this.setState({ showGroupPanel: !this.state.showGroupPanel });
}
break;
}
}
static getSharedInstance(): RightPanelStore {
if (!RightPanelStore._instance) {
RightPanelStore._instance = new RightPanelStore();
if (!RightPanelStore.instance) {
RightPanelStore.instance = new RightPanelStore();
}
return RightPanelStore._instance;
return RightPanelStore.instance;
}
}

View file

@ -15,28 +15,28 @@ limitations under the License.
*/
// These are in their own file because of circular imports being a problem.
export const RIGHT_PANEL_PHASES = Object.freeze({
export enum RightPanelPhases {
// Room stuff
RoomMemberList: 'RoomMemberList',
FilePanel: 'FilePanel',
NotificationPanel: 'NotificationPanel',
RoomMemberInfo: 'RoomMemberInfo',
EncryptionPanel: 'EncryptionPanel',
RoomMemberList = 'RoomMemberList',
FilePanel = 'FilePanel',
NotificationPanel = 'NotificationPanel',
RoomMemberInfo = 'RoomMemberInfo',
EncryptionPanel = 'EncryptionPanel',
Room3pidMemberInfo: 'Room3pidMemberInfo',
Room3pidMemberInfo = 'Room3pidMemberInfo',
// Group stuff
GroupMemberList: 'GroupMemberList',
GroupRoomList: 'GroupRoomList',
GroupRoomInfo: 'GroupRoomInfo',
GroupMemberInfo: 'GroupMemberInfo',
});
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,
RightPanelPhases.NotificationPanel,
RightPanelPhases.FilePanel,
RightPanelPhases.RoomMemberList,
RightPanelPhases.GroupMemberList,
RightPanelPhases.GroupRoomList,
];

View file

@ -19,10 +19,11 @@ import dis from "./dispatcher/dispatcher";
import Modal from './Modal';
import * as sdk from './index';
import { _t } from './languageHandler';
import {RIGHT_PANEL_PHASES} from "./stores/RightPanelStorePhases";
import {RightPanelPhases} from "./stores/RightPanelStorePhases";
import {findDMForUser} from './createRoom';
import {accessSecretStorage} from './CrossSigningManager';
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
import {Action} from './dispatcher/actions';
async function enable4SIfNeeded() {
const cli = MatrixClientPeg.get();
@ -91,8 +92,8 @@ export async function verifyDevice(user, device) {
verificationMethods.SAS,
);
dis.dispatch({
action: "set_right_panel_phase",
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.EncryptionPanel,
refireParams: {member: user, verificationRequestPromise},
});
} else if (action === "legacy") {
@ -120,8 +121,8 @@ export async function legacyVerifyUser(user) {
}
const verificationRequestPromise = cli.requestVerification(user.userId);
dis.dispatch({
action: "set_right_panel_phase",
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.EncryptionPanel,
refireParams: {member: user, verificationRequestPromise},
});
}
@ -132,8 +133,8 @@ export async function verifyUser(user) {
}
const existingRequest = pendingVerificationRequestForUser(user);
dis.dispatch({
action: "set_right_panel_phase",
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.EncryptionPanel,
refireParams: {
member: user,
verificationRequest: existingRequest,