Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/19010
Conflicts: src/components/structures/SpaceRoomView.tsx src/components/views/rooms/MemberList.tsx src/components/views/rooms/RoomList.tsx
This commit is contained in:
commit
020eca6922
177 changed files with 6615 additions and 2121 deletions
|
@ -19,6 +19,7 @@ limitations under the License.
|
|||
import React, { CSSProperties, RefObject, SyntheticEvent, useRef, useState } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import classNames from "classnames";
|
||||
import FocusLock from "react-focus-lock";
|
||||
|
||||
import { Key } from "../../Keyboard";
|
||||
import { Writeable } from "../../@types/common";
|
||||
|
@ -43,8 +44,6 @@ function getOrCreateContainer(): HTMLDivElement {
|
|||
return container;
|
||||
}
|
||||
|
||||
const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]);
|
||||
|
||||
export interface IPosition {
|
||||
top?: number;
|
||||
bottom?: number;
|
||||
|
@ -84,6 +83,10 @@ export interface IProps extends IPosition {
|
|||
// it will be mounted to a container at the root of the DOM.
|
||||
mountAsChild?: boolean;
|
||||
|
||||
// If specified, contents will be wrapped in a FocusLock, this is only needed if the context menu is being rendered
|
||||
// within an existing FocusLock e.g inside a modal.
|
||||
focusLock?: boolean;
|
||||
|
||||
// Function to be called on menu close
|
||||
onFinished();
|
||||
// on resize callback
|
||||
|
@ -99,7 +102,7 @@ interface IState {
|
|||
// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
|
||||
@replaceableComponent("structures.ContextMenu")
|
||||
export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||
private initialFocus: HTMLElement;
|
||||
private readonly initialFocus: HTMLElement;
|
||||
|
||||
static defaultProps = {
|
||||
hasBackground: true,
|
||||
|
@ -108,6 +111,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
contextMenuElem: null,
|
||||
};
|
||||
|
@ -121,14 +125,13 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
this.initialFocus.focus();
|
||||
}
|
||||
|
||||
private collectContextMenuRect = (element) => {
|
||||
private collectContextMenuRect = (element: HTMLDivElement) => {
|
||||
// We don't need to clean up when unmounting, so ignore
|
||||
if (!element) return;
|
||||
|
||||
let first = element.querySelector('[role^="menuitem"]');
|
||||
if (!first) {
|
||||
first = element.querySelector('[tab-index]');
|
||||
}
|
||||
const first = element.querySelector<HTMLElement>('[role^="menuitem"]')
|
||||
|| element.querySelector<HTMLElement>('[tab-index]');
|
||||
|
||||
if (first) {
|
||||
first.focus();
|
||||
}
|
||||
|
@ -205,7 +208,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
descending = true;
|
||||
}
|
||||
}
|
||||
} while (element && !ARIA_MENU_ITEM_ROLES.has(element.getAttribute("role")));
|
||||
} while (element && !element.getAttribute("role")?.startsWith("menuitem"));
|
||||
|
||||
if (element) {
|
||||
(element as HTMLElement).focus();
|
||||
|
@ -226,6 +229,11 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onClick = (ev: React.MouseEvent) => {
|
||||
// Don't allow clicks to escape the context menu wrapper
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
// don't let keyboard handling escape the context menu
|
||||
ev.stopPropagation();
|
||||
|
@ -378,11 +386,23 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
let body = <>
|
||||
{ chevron }
|
||||
{ props.children }
|
||||
</>;
|
||||
|
||||
if (props.focusLock) {
|
||||
body = <FocusLock>
|
||||
{ body }
|
||||
</FocusLock>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames("mx_ContextualMenu_wrapper", this.props.wrapperClassName)}
|
||||
style={{ ...position, ...wrapperStyle }}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onClick={this.onClick}
|
||||
onContextMenu={this.onContextMenuPreventBubbling}
|
||||
>
|
||||
<div
|
||||
|
@ -391,8 +411,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
|||
ref={this.collectContextMenuRect}
|
||||
role={this.props.managed ? "menu" : undefined}
|
||||
>
|
||||
{ chevron }
|
||||
{ props.children }
|
||||
{ body }
|
||||
</div>
|
||||
{ background }
|
||||
</div>
|
||||
|
|
|
@ -37,6 +37,7 @@ import TimelinePanel from "./TimelinePanel";
|
|||
import Spinner from "../views/elements/Spinner";
|
||||
import { TileShape } from '../views/rooms/EventTile';
|
||||
import { Layout } from "../../settings/Layout";
|
||||
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
|
@ -57,6 +58,7 @@ class FilePanel extends React.Component<IProps, IState> {
|
|||
// added to the timeline.
|
||||
private decryptingEvents = new Set<string>();
|
||||
public noRoom: boolean;
|
||||
static contextType = RoomContext;
|
||||
|
||||
state = {
|
||||
timelineSet: null,
|
||||
|
@ -249,38 +251,46 @@ class FilePanel extends React.Component<IProps, IState> {
|
|||
const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
|
||||
|
||||
if (this.state.timelineSet) {
|
||||
// console.log("rendering TimelinePanel for timelineSet " + this.state.timelineSet.room.roomId + " " +
|
||||
// "(" + this.state.timelineSet._timelines.join(", ") + ")" + " with key " + this.props.roomId);
|
||||
return (
|
||||
<BaseCard
|
||||
className="mx_FilePanel"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
withoutScrollContainer
|
||||
>
|
||||
<DesktopBuildsNotice isRoomEncrypted={isRoomEncrypted} kind={WarningKind.Files} />
|
||||
<TimelinePanel
|
||||
manageReadReceipts={false}
|
||||
manageReadMarkers={false}
|
||||
timelineSet={this.state.timelineSet}
|
||||
showUrlPreview={false}
|
||||
onPaginationRequest={this.onPaginationRequest}
|
||||
tileShape={TileShape.FileGrid}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
empty={emptyState}
|
||||
layout={Layout.Group}
|
||||
/>
|
||||
</BaseCard>
|
||||
<RoomContext.Provider value={{
|
||||
...this.context,
|
||||
timelineRenderingType: TimelineRenderingType.File,
|
||||
}}>
|
||||
<BaseCard
|
||||
className="mx_FilePanel"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
withoutScrollContainer
|
||||
>
|
||||
<DesktopBuildsNotice isRoomEncrypted={isRoomEncrypted} kind={WarningKind.Files} />
|
||||
<TimelinePanel
|
||||
manageReadReceipts={false}
|
||||
manageReadMarkers={false}
|
||||
timelineSet={this.state.timelineSet}
|
||||
showUrlPreview={false}
|
||||
onPaginationRequest={this.onPaginationRequest}
|
||||
tileShape={TileShape.FileGrid}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
empty={emptyState}
|
||||
layout={Layout.Group}
|
||||
/>
|
||||
</BaseCard>
|
||||
</RoomContext.Provider>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<BaseCard
|
||||
className="mx_FilePanel"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
>
|
||||
<Spinner />
|
||||
</BaseCard>
|
||||
<RoomContext.Provider value={{
|
||||
...this.context,
|
||||
timelineRenderingType: TimelineRenderingType.File,
|
||||
}}>
|
||||
<BaseCard
|
||||
className="mx_FilePanel"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
>
|
||||
<Spinner />
|
||||
</BaseCard>
|
||||
</RoomContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ interface IState {
|
|||
stageState?: IStageStatus;
|
||||
busy: boolean;
|
||||
errorText?: string;
|
||||
stageErrorText?: string;
|
||||
errorCode?: string;
|
||||
submitButtonEnabled: boolean;
|
||||
}
|
||||
|
||||
|
@ -103,7 +103,7 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
|||
authStage: null,
|
||||
busy: false,
|
||||
errorText: null,
|
||||
stageErrorText: null,
|
||||
errorCode: null,
|
||||
submitButtonEnabled: false,
|
||||
};
|
||||
|
||||
|
@ -145,6 +145,7 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
|||
const msg = error.message || error.toString();
|
||||
this.setState({
|
||||
errorText: msg,
|
||||
errorCode: error.errcode,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -186,6 +187,7 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
|||
authStage: stageType,
|
||||
stageState: stageState,
|
||||
errorText: stageState.error,
|
||||
errorCode: stageState.errcode,
|
||||
}, () => {
|
||||
if (oldStage !== stageType) {
|
||||
this.setFocus();
|
||||
|
@ -208,7 +210,7 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
|||
this.setState({
|
||||
busy: true,
|
||||
errorText: null,
|
||||
stageErrorText: null,
|
||||
errorCode: null,
|
||||
});
|
||||
}
|
||||
// The JS SDK eagerly reports itself as "not busy" right after any
|
||||
|
@ -235,7 +237,15 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
|||
this.props.onAuthFinished(false, ERROR_USER_CANCELLED);
|
||||
};
|
||||
|
||||
private renderCurrentStage(): JSX.Element {
|
||||
private onAuthStageFailed = (e: Error): void => {
|
||||
this.props.onAuthFinished(false, e);
|
||||
};
|
||||
|
||||
private setEmailSid = (sid: string): void => {
|
||||
this.authLogic.setEmailSid(sid);
|
||||
};
|
||||
|
||||
render() {
|
||||
const stage = this.state.authStage;
|
||||
if (!stage) {
|
||||
if (this.state.busy) {
|
||||
|
@ -255,7 +265,8 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
|||
clientSecret={this.authLogic.getClientSecret()}
|
||||
stageParams={this.authLogic.getStageParams(stage)}
|
||||
submitAuthDict={this.submitAuthDict}
|
||||
errorText={this.state.stageErrorText}
|
||||
errorText={this.state.errorText}
|
||||
errorCode={this.state.errorCode}
|
||||
busy={this.state.busy}
|
||||
inputs={this.props.inputs}
|
||||
stageState={this.state.stageState}
|
||||
|
@ -269,32 +280,4 @@ export default class InteractiveAuthComponent extends React.Component<IProps, IS
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private onAuthStageFailed = (e: Error): void => {
|
||||
this.props.onAuthFinished(false, e);
|
||||
};
|
||||
|
||||
private setEmailSid = (sid: string): void => {
|
||||
this.authLogic.setEmailSid(sid);
|
||||
};
|
||||
|
||||
render() {
|
||||
let error = null;
|
||||
if (this.state.errorText) {
|
||||
error = (
|
||||
<div className="error">
|
||||
{ this.state.errorText }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
{ this.renderCurrentStage() }
|
||||
{ error }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ import linkifyMatrix from "../../linkify-matrix";
|
|||
import * as Lifecycle from '../../Lifecycle';
|
||||
// LifecycleStore is not used but does listen to and dispatch actions
|
||||
import '../../stores/LifecycleStore';
|
||||
import PageTypes from '../../PageTypes';
|
||||
import PageType from '../../PageTypes';
|
||||
|
||||
import createRoom, { IOpts } from "../../createRoom";
|
||||
import { _t, _td, getCurrentLanguage } from '../../languageHandler';
|
||||
|
@ -208,7 +208,7 @@ interface IState {
|
|||
view: Views;
|
||||
// What the LoggedInView would be showing if visible
|
||||
// eslint-disable-next-line camelcase
|
||||
page_type?: PageTypes;
|
||||
page_type?: PageType;
|
||||
// The ID of the room we're viewing. This is either populated directly
|
||||
// in the case where we view a room by ID or by RoomView when it resolves
|
||||
// what ID an alias points at.
|
||||
|
@ -724,7 +724,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
break;
|
||||
}
|
||||
case 'view_my_groups':
|
||||
this.setPage(PageTypes.MyGroups);
|
||||
this.setPage(PageType.MyGroups);
|
||||
this.notifyNewScreen('groups');
|
||||
break;
|
||||
case 'view_group':
|
||||
|
@ -763,7 +763,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
localStorage.setItem("mx_seenSpacesBeta", "1");
|
||||
// We just dispatch the page change rather than have to worry about
|
||||
// what the logic is for each of these branches.
|
||||
if (this.state.page_type === PageTypes.MyGroups) {
|
||||
if (this.state.page_type === PageType.MyGroups) {
|
||||
dis.dispatch({ action: 'view_last_screen' });
|
||||
} else {
|
||||
dis.dispatch({ action: 'view_my_groups' });
|
||||
|
@ -849,7 +849,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private setPage(pageType: string) {
|
||||
private setPage(pageType: PageType) {
|
||||
this.setState({
|
||||
page_type: pageType,
|
||||
});
|
||||
|
@ -956,7 +956,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.setState({
|
||||
view: Views.LOGGED_IN,
|
||||
currentRoomId: roomInfo.room_id || null,
|
||||
page_type: PageTypes.RoomView,
|
||||
page_type: PageType.RoomView,
|
||||
threepidInvite: roomInfo.threepid_invite,
|
||||
roomOobData: roomInfo.oob_data,
|
||||
ready: true,
|
||||
|
@ -984,7 +984,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
currentGroupId: groupId,
|
||||
currentGroupIsNew: payload.group_is_new,
|
||||
});
|
||||
this.setPage(PageTypes.GroupView);
|
||||
this.setPage(PageType.GroupView);
|
||||
this.notifyNewScreen('group/' + groupId);
|
||||
}
|
||||
|
||||
|
@ -1027,7 +1027,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
justRegistered,
|
||||
currentRoomId: null,
|
||||
});
|
||||
this.setPage(PageTypes.HomePage);
|
||||
this.setPage(PageType.HomePage);
|
||||
this.notifyNewScreen('home');
|
||||
ThemeController.isLogin = false;
|
||||
this.themeWatcher.recheck();
|
||||
|
@ -1045,7 +1045,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
this.notifyNewScreen('user/' + userId);
|
||||
this.setState({ currentUserId: userId });
|
||||
this.setPage(PageTypes.UserView);
|
||||
this.setPage(PageType.UserView);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -48,6 +48,8 @@ import Spinner from "../views/elements/Spinner";
|
|||
import TileErrorBoundary from '../views/messages/TileErrorBoundary';
|
||||
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
|
||||
import EditorStateTransfer from "../../utils/EditorStateTransfer";
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
import { Action } from '../../dispatcher/actions';
|
||||
|
||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
|
||||
|
@ -60,7 +62,7 @@ const groupedEvents = [
|
|||
|
||||
// check if there is a previous event and it has the same sender as this event
|
||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||
function shouldFormContinuation(
|
||||
export function shouldFormContinuation(
|
||||
prevEvent: MatrixEvent,
|
||||
mxEvent: MatrixEvent,
|
||||
showHiddenEvents: boolean,
|
||||
|
@ -287,6 +289,15 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
ghostReadMarkers,
|
||||
});
|
||||
}
|
||||
|
||||
const pendingEditItem = this.pendingEditItem;
|
||||
if (!this.props.editState && this.props.room && pendingEditItem) {
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.EditEvent,
|
||||
event: this.props.room.findEventById(pendingEditItem),
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private calculateRoomMembersCount = (): void => {
|
||||
|
@ -550,10 +561,14 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
return { nextEvent, nextTile };
|
||||
}
|
||||
|
||||
private get roomHasPendingEdit(): string {
|
||||
return this.props.room && localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`);
|
||||
private get pendingEditItem(): string | undefined {
|
||||
try {
|
||||
return localStorage.getItem(`mx_edit_room_${this.props.room.roomId}_${this.context.timelineRenderingType}`);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private getEventTiles(): ReactNode[] {
|
||||
this.eventNodes = {};
|
||||
|
||||
|
@ -663,13 +678,6 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
if (!this.props.editState && this.roomHasPendingEdit) {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "edit_event",
|
||||
event: this.props.room.findEventById(this.roomHasPendingEdit),
|
||||
});
|
||||
}
|
||||
|
||||
if (grouper) {
|
||||
ret.push(...grouper.getTiles());
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import TimelinePanel from "./TimelinePanel";
|
|||
import Spinner from "../views/elements/Spinner";
|
||||
import { TileShape } from "../views/rooms/EventTile";
|
||||
import { Layout } from "../../settings/Layout";
|
||||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
|
||||
interface IProps {
|
||||
onClose(): void;
|
||||
|
@ -34,6 +35,7 @@ interface IProps {
|
|||
*/
|
||||
@replaceableComponent("structures.NotificationPanel")
|
||||
export default class NotificationPanel extends React.PureComponent<IProps> {
|
||||
static contextType = RoomContext;
|
||||
render() {
|
||||
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
|
||||
<h2>{ _t('You’re all caught up') }</h2>
|
||||
|
@ -61,8 +63,13 @@ export default class NotificationPanel extends React.PureComponent<IProps> {
|
|||
content = <Spinner />;
|
||||
}
|
||||
|
||||
return <BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
|
||||
{ content }
|
||||
</BaseCard>;
|
||||
return <RoomContext.Provider value={{
|
||||
...this.context,
|
||||
timelineRenderingType: TimelineRenderingType.Notification,
|
||||
}}>
|
||||
<BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
|
||||
{ content }
|
||||
</BaseCard>
|
||||
</RoomContext.Provider>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,8 +48,8 @@ import { Layout } from "../../settings/Layout";
|
|||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import { haveTileForEvent } from "../views/rooms/EventTile";
|
||||
import RoomContext from "../../contexts/RoomContext";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
import MatrixClientContext, { withMatrixClientHOC, MatrixClientProps } from "../../contexts/MatrixClientContext";
|
||||
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||
|
@ -91,6 +91,7 @@ import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
|
|||
import SpaceStore from "../../stores/SpaceStore";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
|
@ -102,7 +103,7 @@ if (DEBUG) {
|
|||
debuglog = logger.log.bind(console);
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
interface IRoomProps extends MatrixClientProps {
|
||||
threepidInvite: IThreepidInvite;
|
||||
oobData?: IOOBData;
|
||||
|
||||
|
@ -113,7 +114,7 @@ interface IProps {
|
|||
onRegistered?(credentials: IMatrixClientCreds): void;
|
||||
}
|
||||
|
||||
export interface IState {
|
||||
export interface IRoomState {
|
||||
room?: Room;
|
||||
roomId?: string;
|
||||
roomAlias?: string;
|
||||
|
@ -187,10 +188,12 @@ export interface IState {
|
|||
// if it did we don't want the room to be marked as read as soon as it is loaded.
|
||||
wasContextSwitch?: boolean;
|
||||
editState?: EditorStateTransfer;
|
||||
timelineRenderingType: TimelineRenderingType;
|
||||
liveTimeline?: EventTimeline;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.RoomView")
|
||||
export default class RoomView extends React.Component<IProps, IState> {
|
||||
export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
private readonly dispatcherRef: string;
|
||||
private readonly roomStoreToken: EventSubscription;
|
||||
private readonly rightPanelStoreToken: EventSubscription;
|
||||
|
@ -247,6 +250,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
showDisplaynameChanges: true,
|
||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||
dragCounter: 0,
|
||||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
liveTimeline: undefined,
|
||||
};
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
@ -336,7 +341,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
const roomId = RoomViewStore.getRoomId();
|
||||
|
||||
const newState: Pick<IState, any> = {
|
||||
const newState: Pick<IRoomState, any> = {
|
||||
roomId,
|
||||
roomAlias: RoomViewStore.getRoomAlias(),
|
||||
roomLoading: RoomViewStore.isRoomLoading(),
|
||||
|
@ -808,7 +813,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
this.onSearchClick();
|
||||
break;
|
||||
|
||||
case "edit_event": {
|
||||
case Action.EditEvent: {
|
||||
// Quit early if we're trying to edit events in wrong rendering context
|
||||
if (payload.timelineRenderingType !== this.state.timelineRenderingType) return;
|
||||
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||
this.setState({ editState }, () => {
|
||||
if (payload.event) {
|
||||
|
@ -932,6 +939,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
this.updateE2EStatus(room);
|
||||
this.updatePermissions(room);
|
||||
this.checkWidgets(room);
|
||||
|
||||
this.setState({
|
||||
liveTimeline: room.getLiveTimeline(),
|
||||
});
|
||||
};
|
||||
|
||||
private async calculateRecommendedVersion(room: Room) {
|
||||
|
@ -2086,3 +2097,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
const RoomViewWithMatrixClient = withMatrixClientHOC(RoomView);
|
||||
export default RoomViewWithMatrixClient;
|
||||
|
|
|
@ -15,17 +15,17 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, {
|
||||
Dispatch,
|
||||
KeyboardEvent,
|
||||
KeyboardEventHandler,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
KeyboardEvent,
|
||||
KeyboardEventHandler,
|
||||
useContext,
|
||||
SetStateAction,
|
||||
Dispatch,
|
||||
} from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy";
|
||||
|
@ -33,7 +33,8 @@ import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
|||
import { IHierarchyRelation, IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import classNames from "classnames";
|
||||
import { sortBy } from "lodash";
|
||||
import { sortBy, uniqBy } from "lodash";
|
||||
import { GuestAccess, HistoryVisibility } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
|
@ -333,6 +334,30 @@ interface IHierarchyLevelProps {
|
|||
onToggleClick?(parentId: string, childId: string): void;
|
||||
}
|
||||
|
||||
const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom): IHierarchyRoom => {
|
||||
const history = cli.getRoomUpgradeHistory(room.room_id, true);
|
||||
const cliRoom = history[history.length - 1];
|
||||
if (cliRoom) {
|
||||
return {
|
||||
...room,
|
||||
room_id: cliRoom.roomId,
|
||||
room_type: cliRoom.getType(),
|
||||
name: cliRoom.name,
|
||||
topic: cliRoom.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent().topic,
|
||||
avatar_url: cliRoom.getMxcAvatarUrl(),
|
||||
canonical_alias: cliRoom.getCanonicalAlias(),
|
||||
aliases: cliRoom.getAltAliases(),
|
||||
world_readable: cliRoom.currentState.getStateEvents(EventType.RoomHistoryVisibility, "")?.getContent()
|
||||
.history_visibility === HistoryVisibility.WorldReadable,
|
||||
guest_can_join: cliRoom.currentState.getStateEvents(EventType.RoomGuestAccess, "")?.getContent()
|
||||
.guest_access === GuestAccess.CanJoin,
|
||||
num_joined_members: cliRoom.getJoinedMemberCount(),
|
||||
};
|
||||
}
|
||||
|
||||
return room;
|
||||
};
|
||||
|
||||
export const HierarchyLevel = ({
|
||||
root,
|
||||
roomSet,
|
||||
|
@ -353,7 +378,7 @@ export const HierarchyLevel = ({
|
|||
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: IHierarchyRelation) => {
|
||||
const room = hierarchy.roomMap.get(ev.state_key);
|
||||
if (room && roomSet.has(room)) {
|
||||
result[room.room_type === RoomType.Space ? 0 : 1].push(room);
|
||||
result[room.room_type === RoomType.Space ? 0 : 1].push(toLocalRoom(cli, room));
|
||||
}
|
||||
return result;
|
||||
}, [[] as IHierarchyRoom[], [] as IHierarchyRoom[]]);
|
||||
|
@ -361,7 +386,7 @@ export const HierarchyLevel = ({
|
|||
const newParents = new Set(parents).add(root.room_id);
|
||||
return <React.Fragment>
|
||||
{
|
||||
childRooms.map(room => (
|
||||
uniqBy(childRooms, "room_id").map(room => (
|
||||
<Tile
|
||||
key={room.room_id}
|
||||
room={room}
|
||||
|
@ -410,50 +435,39 @@ export const HierarchyLevel = ({
|
|||
|
||||
const INITIAL_PAGE_SIZE = 20;
|
||||
|
||||
export const useSpaceSummary = (space: Room): {
|
||||
export const useRoomHierarchy = (space: Room): {
|
||||
loading: boolean;
|
||||
rooms: IHierarchyRoom[];
|
||||
hierarchy: RoomHierarchy;
|
||||
loadMore(pageSize?: number): Promise <void>;
|
||||
} => {
|
||||
const [rooms, setRooms] = useState<IHierarchyRoom[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [hierarchy, setHierarchy] = useState<RoomHierarchy>();
|
||||
|
||||
const resetHierarchy = useCallback(() => {
|
||||
const hierarchy = new RoomHierarchy(space, INITIAL_PAGE_SIZE);
|
||||
setHierarchy(hierarchy);
|
||||
|
||||
let discard = false;
|
||||
hierarchy.load().then(() => {
|
||||
if (discard) return;
|
||||
if (space !== hierarchy.root) return; // discard stale results
|
||||
setRooms(hierarchy.rooms);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
discard = true;
|
||||
};
|
||||
setHierarchy(hierarchy);
|
||||
}, [space]);
|
||||
useEffect(resetHierarchy, [resetHierarchy]);
|
||||
|
||||
useDispatcher(defaultDispatcher, (payload => {
|
||||
if (payload.action === Action.UpdateSpaceHierarchy) {
|
||||
setLoading(true);
|
||||
setRooms([]); // TODO
|
||||
resetHierarchy();
|
||||
}
|
||||
}));
|
||||
|
||||
const loadMore = useCallback(async (pageSize?: number) => {
|
||||
if (!hierarchy.canLoadMore || hierarchy.noSupport) return;
|
||||
|
||||
setLoading(true);
|
||||
if (hierarchy.loading || !hierarchy.canLoadMore || hierarchy.noSupport) return;
|
||||
await hierarchy.load(pageSize);
|
||||
setRooms(hierarchy.rooms);
|
||||
setLoading(false);
|
||||
}, [hierarchy]);
|
||||
|
||||
const loading = hierarchy?.loading ?? true;
|
||||
return { loading, rooms, hierarchy, loadMore };
|
||||
};
|
||||
|
||||
|
@ -587,7 +601,7 @@ const SpaceHierarchy = ({
|
|||
|
||||
const [selected, setSelected] = useState(new Map<string, Set<string>>()); // Map<parentId, Set<childId>>
|
||||
|
||||
const { loading, rooms, hierarchy, loadMore } = useSpaceSummary(space);
|
||||
const { loading, rooms, hierarchy, loadMore } = useRoomHierarchy(space);
|
||||
|
||||
const filteredRoomSet = useMemo<Set<IHierarchyRoom>>(() => {
|
||||
if (!rooms?.length) return new Set();
|
||||
|
@ -648,8 +662,6 @@ const SpaceHierarchy = ({
|
|||
return <RovingTabIndexProvider onKeyDown={onKeyDown} handleHomeEnd handleUpDown>
|
||||
{ ({ onKeyDownHandler }) => {
|
||||
let content: JSX.Element;
|
||||
let loader: JSX.Element;
|
||||
|
||||
if (loading && !rooms.length) {
|
||||
content = <Spinner />;
|
||||
} else {
|
||||
|
@ -671,19 +683,20 @@ const SpaceHierarchy = ({
|
|||
}}
|
||||
/>
|
||||
</>;
|
||||
|
||||
if (hierarchy.canLoadMore) {
|
||||
loader = <div ref={loaderRef}>
|
||||
<Spinner />
|
||||
</div>;
|
||||
}
|
||||
} else {
|
||||
} else if (!hierarchy.canLoadMore) {
|
||||
results = <div className="mx_SpaceHierarchy_noResults">
|
||||
<h3>{ _t("No results found") }</h3>
|
||||
<div>{ _t("You may want to try a different search or check for typos.") }</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
let loader: JSX.Element;
|
||||
if (hierarchy.canLoadMore) {
|
||||
loader = <div ref={loaderRef}>
|
||||
<Spinner />
|
||||
</div>;
|
||||
}
|
||||
|
||||
content = <>
|
||||
<div className="mx_SpaceHierarchy_listHeader">
|
||||
<h4>{ query.trim() ? _t("Results") : _t("Rooms and spaces") }</h4>
|
||||
|
|
|
@ -82,6 +82,8 @@ import GroupAvatar from "../views/avatars/GroupAvatar";
|
|||
import { useDispatcher } from "../../hooks/useDispatcher";
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { shouldShowComponent } from "../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../settings/UIFeature";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
|
@ -412,7 +414,9 @@ const SpaceLanding = ({ space }: { space: Room }) => {
|
|||
const userId = cli.getUserId();
|
||||
|
||||
let inviteButton;
|
||||
if ((myMembership === "join" && space.canInvite(userId)) || space.getJoinRule() === JoinRule.Public) {
|
||||
if (((myMembership === "join" && space.canInvite(userId)) || space.getJoinRule() === JoinRule.Public) &&
|
||||
shouldShowComponent(UIComponent.InviteUsers)
|
||||
) {
|
||||
inviteButton = (
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
|
@ -730,7 +734,6 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
|
|||
</div>
|
||||
|
||||
<div className="mx_SpaceRoomView_inviteTeammates_betaDisclaimer">
|
||||
<BetaPill />
|
||||
{ _t("<b>This is an experimental feature.</b> For now, " +
|
||||
"new users receiving an invite will have to open the invite on <link/> to actually join.", {}, {
|
||||
b: sub => <b>{ sub }</b>,
|
||||
|
|
|
@ -34,6 +34,8 @@ import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPan
|
|||
import { Action } from '../../dispatcher/actions';
|
||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||
import { E2EStatus } from '../../utils/ShieldUtils';
|
||||
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||
import RoomContext, { TimelineRenderingType } from '../../contexts/RoomContext';
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -47,10 +49,14 @@ interface IProps {
|
|||
interface IState {
|
||||
replyToEvent?: MatrixEvent;
|
||||
thread?: Thread;
|
||||
editState?: EditorStateTransfer;
|
||||
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.ThreadView")
|
||||
export default class ThreadView extends React.Component<IProps, IState> {
|
||||
static contextType = RoomContext;
|
||||
|
||||
private dispatcherRef: string;
|
||||
private timelinePanelRef: React.RefObject<TimelinePanel> = React.createRef();
|
||||
|
||||
|
@ -90,6 +96,23 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
this.setupThread(payload.event);
|
||||
}
|
||||
}
|
||||
switch (payload.action) {
|
||||
case Action.EditEvent: {
|
||||
// Quit early if it's not a thread context
|
||||
if (payload.timelineRenderingType !== TimelineRenderingType.Thread) return;
|
||||
// Quit early if that's not a thread event
|
||||
if (payload.event && !payload.event.getThread()) return;
|
||||
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||
this.setState({ editState }, () => {
|
||||
if (payload.event) {
|
||||
this.timelinePanelRef.current?.scrollToEventIfNeeded(payload.event.getId());
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private setupThread = (mxEv: MatrixEvent) => {
|
||||
|
@ -124,44 +147,53 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<BaseCard
|
||||
className="mx_ThreadView"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
withoutScrollContainer={true}
|
||||
>
|
||||
{ this.state.thread && (
|
||||
<TimelinePanel
|
||||
ref={this.timelinePanelRef}
|
||||
showReadReceipts={false} // No RR support in thread's MVP
|
||||
manageReadReceipts={false} // No RR support in thread's MVP
|
||||
manageReadMarkers={false} // No RM support in thread's MVP
|
||||
sendReadReceiptOnLoad={false} // No RR support in thread's MVP
|
||||
timelineSet={this.state?.thread?.timelineSet}
|
||||
showUrlPreview={true}
|
||||
tileShape={TileShape.Thread}
|
||||
empty={<div>empty</div>}
|
||||
alwaysShowTimestamps={true}
|
||||
layout={Layout.Group}
|
||||
hideThreadedMessages={false}
|
||||
hidden={false}
|
||||
showReactions={true}
|
||||
className="mx_RoomView_messagePanel mx_GroupLayout"
|
||||
<RoomContext.Provider value={{
|
||||
...this.context,
|
||||
timelineRenderingType: TimelineRenderingType.Thread,
|
||||
liveTimeline: this.state?.thread?.timelineSet?.getLiveTimeline(),
|
||||
}}>
|
||||
|
||||
<BaseCard
|
||||
className="mx_ThreadView"
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
withoutScrollContainer={true}
|
||||
>
|
||||
{ this.state.thread && (
|
||||
<TimelinePanel
|
||||
ref={this.timelinePanelRef}
|
||||
showReadReceipts={false} // No RR support in thread's MVP
|
||||
manageReadReceipts={false} // No RR support in thread's MVP
|
||||
manageReadMarkers={false} // No RM support in thread's MVP
|
||||
sendReadReceiptOnLoad={false} // No RR support in thread's MVP
|
||||
timelineSet={this.state?.thread?.timelineSet}
|
||||
showUrlPreview={true}
|
||||
tileShape={TileShape.Thread}
|
||||
empty={<div>empty</div>}
|
||||
alwaysShowTimestamps={true}
|
||||
layout={Layout.Group}
|
||||
hideThreadedMessages={false}
|
||||
hidden={false}
|
||||
showReactions={true}
|
||||
className="mx_RoomView_messagePanel mx_GroupLayout"
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
membersLoaded={true}
|
||||
editState={this.state.editState}
|
||||
/>
|
||||
) }
|
||||
|
||||
{ this.state?.thread?.timelineSet && (<MessageComposer
|
||||
room={this.props.room}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
replyInThread={true}
|
||||
replyToEvent={this.state?.thread?.replyToEvent}
|
||||
showReplyPreview={false}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
membersLoaded={true}
|
||||
/>
|
||||
) }
|
||||
<MessageComposer
|
||||
room={this.props.room}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
replyInThread={true}
|
||||
replyToEvent={this.state?.thread?.replyToEvent}
|
||||
showReplyPreview={false}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
compact={true}
|
||||
/>
|
||||
</BaseCard>
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
compact={true}
|
||||
/>) }
|
||||
</BaseCard>
|
||||
</RoomContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import * as sdk from '../../../index';
|
|||
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
|
||||
import SetupEncryptionBody from "./SetupEncryptionBody";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import AccessibleButton from '../../views/elements/AccessibleButton';
|
||||
|
||||
interface IProps {
|
||||
onFinished: () => void;
|
||||
|
@ -27,6 +28,7 @@ interface IProps {
|
|||
|
||||
interface IState {
|
||||
phase: Phase;
|
||||
lostKeys: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.auth.CompleteSecurity")
|
||||
|
@ -36,12 +38,17 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
|||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.on("update", this.onStoreUpdate);
|
||||
store.start();
|
||||
this.state = { phase: store.phase };
|
||||
this.state = { phase: store.phase, lostKeys: store.lostKeys() };
|
||||
}
|
||||
|
||||
private onStoreUpdate = (): void => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
this.setState({ phase: store.phase });
|
||||
this.setState({ phase: store.phase, lostKeys: store.lostKeys() });
|
||||
};
|
||||
|
||||
private onSkipClick = (): void => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.skip();
|
||||
};
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
|
@ -53,15 +60,20 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
|||
public render() {
|
||||
const AuthPage = sdk.getComponent("auth.AuthPage");
|
||||
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
|
||||
const { phase } = this.state;
|
||||
const { phase, lostKeys } = this.state;
|
||||
let icon;
|
||||
let title;
|
||||
|
||||
if (phase === Phase.Loading) {
|
||||
return null;
|
||||
} else if (phase === Phase.Intro) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("Verify this login");
|
||||
if (lostKeys) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("Unable to verify this login");
|
||||
} else {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("Verify this login");
|
||||
}
|
||||
} else if (phase === Phase.Done) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
|
||||
title = _t("Session verified");
|
||||
|
@ -71,16 +83,29 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
|||
} else if (phase === Phase.Busy) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("Verify this login");
|
||||
} else if (phase === Phase.ConfirmReset) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("Really reset verification keys?");
|
||||
} else if (phase === Phase.Finished) {
|
||||
// SetupEncryptionBody will take care of calling onFinished, we don't need to do anything
|
||||
} else {
|
||||
throw new Error(`Unknown phase ${phase}`);
|
||||
}
|
||||
|
||||
let skipButton;
|
||||
if (phase === Phase.Intro || phase === Phase.ConfirmReset) {
|
||||
skipButton = (
|
||||
<AccessibleButton onClick={this.onSkipClick} className="mx_CompleteSecurity_skip" aria-label={_t("Skip verification for now")} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthPage>
|
||||
<CompleteSecurityBody>
|
||||
<h2 className="mx_CompleteSecurity_header">
|
||||
{ icon }
|
||||
{ title }
|
||||
{ skipButton }
|
||||
</h2>
|
||||
<div className="mx_CompleteSecurity_body">
|
||||
<SetupEncryptionBody onFinished={this.props.onFinished} />
|
||||
|
|
|
@ -46,6 +46,7 @@ interface IState {
|
|||
phase: Phase;
|
||||
verificationRequest: VerificationRequest;
|
||||
backupInfo: IKeyBackupInfo;
|
||||
lostKeys: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.auth.SetupEncryptionBody")
|
||||
|
@ -62,6 +63,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
// Because of the latter, it lives in the state.
|
||||
verificationRequest: store.verificationRequest,
|
||||
backupInfo: store.backupInfo,
|
||||
lostKeys: store.lostKeys(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -75,6 +77,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
phase: store.phase,
|
||||
verificationRequest: store.verificationRequest,
|
||||
backupInfo: store.backupInfo,
|
||||
lostKeys: store.lostKeys(),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -105,11 +108,6 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
});
|
||||
};
|
||||
|
||||
private onSkipClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.skip();
|
||||
};
|
||||
|
||||
private onSkipConfirmClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.skipConfirm();
|
||||
|
@ -120,6 +118,22 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
store.returnAfterSkip();
|
||||
};
|
||||
|
||||
private onResetClick = (ev: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
ev.preventDefault();
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.reset();
|
||||
};
|
||||
|
||||
private onResetConfirmClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.resetConfirm();
|
||||
};
|
||||
|
||||
private onResetBackClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.returnAfterReset();
|
||||
};
|
||||
|
||||
private onDoneClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.done();
|
||||
|
@ -132,6 +146,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
public render() {
|
||||
const {
|
||||
phase,
|
||||
lostKeys,
|
||||
} = this.state;
|
||||
|
||||
if (this.state.verificationRequest) {
|
||||
|
@ -143,43 +158,67 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
isRoomEncrypted={false}
|
||||
/>;
|
||||
} else if (phase === Phase.Intro) {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
let recoveryKeyPrompt;
|
||||
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
|
||||
recoveryKeyPrompt = _t("Use Security Key or Phrase");
|
||||
} else if (store.keyInfo) {
|
||||
recoveryKeyPrompt = _t("Use Security Key");
|
||||
}
|
||||
if (lostKeys) {
|
||||
return (
|
||||
<div>
|
||||
<p>{ _t(
|
||||
"It looks like you don't have a Security Key or any other devices you can " +
|
||||
"verify against. This device will not be able to access old encrypted messages. " +
|
||||
"In order to verify your identity on this device, you'll need to reset " +
|
||||
"your verification keys.",
|
||||
) }</p>
|
||||
|
||||
let useRecoveryKeyButton;
|
||||
if (recoveryKeyPrompt) {
|
||||
useRecoveryKeyButton = <AccessibleButton kind="link" onClick={this.onUsePassphraseClick}>
|
||||
{ recoveryKeyPrompt }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let verifyButton;
|
||||
if (store.hasDevicesToVerifyAgainst) {
|
||||
verifyButton = <AccessibleButton kind="primary" onClick={this.onVerifyClick}>
|
||||
{ _t("Use another login") }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{ _t(
|
||||
"Verify your identity to access encrypted messages and prove your identity to others.",
|
||||
) }</p>
|
||||
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
{ verifyButton }
|
||||
{ useRecoveryKeyButton }
|
||||
<AccessibleButton kind="danger" onClick={this.onSkipClick}>
|
||||
{ _t("Skip") }
|
||||
</AccessibleButton>
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton kind="primary" onClick={this.onResetConfirmClick}>
|
||||
{ _t("Proceed with reset") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
} else {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
let recoveryKeyPrompt;
|
||||
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
|
||||
recoveryKeyPrompt = _t("Verify with Security Key or Phrase");
|
||||
} else if (store.keyInfo) {
|
||||
recoveryKeyPrompt = _t("Verify with Security Key");
|
||||
}
|
||||
|
||||
let useRecoveryKeyButton;
|
||||
if (recoveryKeyPrompt) {
|
||||
useRecoveryKeyButton = <AccessibleButton kind="primary" onClick={this.onUsePassphraseClick}>
|
||||
{ recoveryKeyPrompt }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let verifyButton;
|
||||
if (store.hasDevicesToVerifyAgainst) {
|
||||
verifyButton = <AccessibleButton kind="primary" onClick={this.onVerifyClick}>
|
||||
{ _t("Verify with another login") }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{ _t(
|
||||
"Verify your identity to access encrypted messages and prove your identity to others.",
|
||||
) }</p>
|
||||
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
{ verifyButton }
|
||||
{ useRecoveryKeyButton }
|
||||
</div>
|
||||
<div className="mx_SetupEncryptionBody_reset">
|
||||
{ _t("Forgotten or lost all recovery methods? <a>Reset all</a>", null, {
|
||||
a: (sub) => <a
|
||||
href=""
|
||||
onClick={this.onResetClick}
|
||||
className="mx_SetupEncryptionBody_reset_link">{ sub }</a>,
|
||||
}) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else if (phase === Phase.Done) {
|
||||
let message;
|
||||
if (this.state.backupInfo) {
|
||||
|
@ -215,14 +254,13 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
) }</p>
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton
|
||||
className="warning"
|
||||
kind="secondary"
|
||||
kind="danger_outline"
|
||||
onClick={this.onSkipConfirmClick}
|
||||
>
|
||||
{ _t("Skip") }
|
||||
{ _t("I'll verify later") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
kind="primary"
|
||||
onClick={this.onSkipBackClick}
|
||||
>
|
||||
{ _t("Go Back") }
|
||||
|
@ -230,6 +268,30 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (phase === Phase.ConfirmReset) {
|
||||
return (
|
||||
<div>
|
||||
<p>{ _t(
|
||||
"Resetting your verification keys cannot be undone. After resetting, " +
|
||||
"you won't have access to old encrypted messages, and any friends who " +
|
||||
"have previously verified you will see security warnings until you " +
|
||||
"re-verify with them.",
|
||||
) }</p>
|
||||
<p>{ _t(
|
||||
"Please only proceed if you're sure you've lost all of your other " +
|
||||
"devices and your security key.",
|
||||
) }</p>
|
||||
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton kind="danger_outline" onClick={this.onResetConfirmClick}>
|
||||
{ _t("Proceed with reset") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="primary" onClick={this.onResetBackClick}>
|
||||
{ _t("Go Back") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (phase === Phase.Busy || phase === Phase.Loading) {
|
||||
return <Spinner />;
|
||||
} else {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue