Merge remote-tracking branch 'upstream/develop' into last-admin-leave-room-warning
This commit is contained in:
commit
b3e535a7aa
223 changed files with 7406 additions and 2005 deletions
|
@ -1,83 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 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 liveIcon from "../../../res/img/element-icons/live.svg";
|
||||
import microphoneIcon from "../../../res/img/voip/call-view/mic-on.svg";
|
||||
import pauseIcon from "../../../res/img/element-icons/pause.svg";
|
||||
import playIcon from "../../../res/img/element-icons/play.svg";
|
||||
import stopIcon from "../../../res/img/element-icons/Stop.svg";
|
||||
|
||||
export enum IconType {
|
||||
Live,
|
||||
Microphone,
|
||||
Pause,
|
||||
Play,
|
||||
Stop,
|
||||
}
|
||||
|
||||
const iconTypeMap = new Map([
|
||||
[IconType.Live, liveIcon],
|
||||
[IconType.Microphone, microphoneIcon],
|
||||
[IconType.Pause, pauseIcon],
|
||||
[IconType.Play, playIcon],
|
||||
[IconType.Stop, stopIcon],
|
||||
]);
|
||||
|
||||
export enum IconColour {
|
||||
Accent = "accent",
|
||||
LiveBadge = "live-badge",
|
||||
CompoundSecondaryContent = "compound-secondary-content",
|
||||
}
|
||||
|
||||
export enum IconSize {
|
||||
S16 = "16",
|
||||
}
|
||||
|
||||
interface IconProps {
|
||||
colour?: IconColour;
|
||||
size?: IconSize;
|
||||
type: IconType;
|
||||
}
|
||||
|
||||
export const Icon: React.FC<IconProps> = ({
|
||||
size = IconSize.S16,
|
||||
colour = IconColour.Accent,
|
||||
type,
|
||||
...rest
|
||||
}) => {
|
||||
const classes = [
|
||||
"mx_Icon",
|
||||
`mx_Icon_${size}`,
|
||||
`mx_Icon_${colour}`,
|
||||
];
|
||||
|
||||
const styles: React.CSSProperties = {
|
||||
maskImage: `url("${iconTypeMap.get(type)}")`,
|
||||
WebkitMaskImage: `url("${iconTypeMap.get(type)}")`,
|
||||
};
|
||||
|
||||
return (
|
||||
<i
|
||||
aria-hidden
|
||||
className={classes.join(" ")}
|
||||
role="presentation"
|
||||
style={styles}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -565,8 +565,13 @@ type ContextMenuTuple<T> = [
|
|||
(val: boolean) => void,
|
||||
];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
|
||||
export const useContextMenu = <T extends any = HTMLElement>(): ContextMenuTuple<T> => {
|
||||
const button = useRef<T>(null);
|
||||
export const useContextMenu = <T extends any = HTMLElement>(inputRef?: RefObject<T>): ContextMenuTuple<T> => {
|
||||
let button = useRef<T>(null);
|
||||
if (inputRef) {
|
||||
// if we are given a ref, use it instead of ours
|
||||
button = inputRef;
|
||||
}
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const open = (ev?: SyntheticEvent) => {
|
||||
ev?.preventDefault();
|
||||
|
@ -579,7 +584,7 @@ export const useContextMenu = <T extends any = HTMLElement>(): ContextMenuTuple<
|
|||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return [isOpen, button, open, close, setIsOpen];
|
||||
return [button.current ? isOpen : false, button, open, close, setIsOpen];
|
||||
};
|
||||
|
||||
// XXX: Deprecated, used only for dynamic Tooltips. Avoid using at all costs.
|
||||
|
|
|
@ -19,7 +19,7 @@ import React, { useEffect, useState } from "react";
|
|||
import { _t } from "../../languageHandler";
|
||||
|
||||
interface IProps {
|
||||
parent: HTMLElement;
|
||||
parent: HTMLElement | null;
|
||||
onFileDrop(dataTransfer: DataTransfer): void;
|
||||
}
|
||||
|
||||
|
@ -90,20 +90,20 @@ const FileDropTarget: React.FC<IProps> = ({ parent, onFileDrop }) => {
|
|||
}));
|
||||
};
|
||||
|
||||
parent.addEventListener("drop", onDrop);
|
||||
parent.addEventListener("dragover", onDragOver);
|
||||
parent.addEventListener("dragenter", onDragEnter);
|
||||
parent.addEventListener("dragleave", onDragLeave);
|
||||
parent?.addEventListener("drop", onDrop);
|
||||
parent?.addEventListener("dragover", onDragOver);
|
||||
parent?.addEventListener("dragenter", onDragEnter);
|
||||
parent?.addEventListener("dragleave", onDragLeave);
|
||||
|
||||
return () => {
|
||||
// disconnect the D&D event listeners from the room view. This
|
||||
// is really just for hygiene - we're going to be
|
||||
// deleted anyway, so it doesn't matter if the event listeners
|
||||
// don't get cleaned up.
|
||||
parent.removeEventListener("drop", onDrop);
|
||||
parent.removeEventListener("dragover", onDragOver);
|
||||
parent.removeEventListener("dragenter", onDragEnter);
|
||||
parent.removeEventListener("dragleave", onDragLeave);
|
||||
parent?.removeEventListener("drop", onDrop);
|
||||
parent?.removeEventListener("dragover", onDragOver);
|
||||
parent?.removeEventListener("dragenter", onDragEnter);
|
||||
parent?.removeEventListener("dragleave", onDragLeave);
|
||||
};
|
||||
}, [parent, onFileDrop]);
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import classNames from 'classnames';
|
|||
import { ISyncStateData, SyncState } from 'matrix-js-sdk/src/sync';
|
||||
import { IUsageLimit } from 'matrix-js-sdk/src/@types/partials';
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { MatrixError } from 'matrix-js-sdk/src/matrix';
|
||||
|
||||
import { isOnlyCtrlOrCmdKeyEvent, Key } from '../../Keyboard';
|
||||
import PageTypes from '../../PageTypes';
|
||||
|
@ -288,8 +289,8 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onSync = (syncState: SyncState, oldSyncState?: SyncState, data?: ISyncStateData): void => {
|
||||
const oldErrCode = this.state.syncErrorData?.error?.errcode;
|
||||
const newErrCode = data && data.error && data.error.errcode;
|
||||
const oldErrCode = (this.state.syncErrorData?.error as MatrixError)?.errcode;
|
||||
const newErrCode = (data?.error as MatrixError)?.errcode;
|
||||
if (syncState === oldSyncState && oldErrCode === newErrCode) return;
|
||||
|
||||
this.setState({
|
||||
|
@ -317,9 +318,9 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
|
||||
const error = syncError && syncError.error && syncError.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
|
||||
const error = (syncError?.error as MatrixError)?.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
|
||||
if (error) {
|
||||
usageLimitEventContent = syncError.error.data as IUsageLimit;
|
||||
usageLimitEventContent = (syncError?.error as MatrixError).data as IUsageLimit;
|
||||
}
|
||||
|
||||
// usageLimitDismissed is true when the user has explicitly hidden the toast
|
||||
|
|
|
@ -24,7 +24,6 @@ import {
|
|||
MatrixEventEvent,
|
||||
} from 'matrix-js-sdk/src/matrix';
|
||||
import { ISyncStateData, SyncState } from 'matrix-js-sdk/src/sync';
|
||||
import { MatrixError } from 'matrix-js-sdk/src/http-api';
|
||||
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { defer, IDeferred, QueryDict } from "matrix-js-sdk/src/utils";
|
||||
|
@ -137,8 +136,10 @@ import { TimelineRenderingType } from "../../contexts/RoomContext";
|
|||
import { UseCaseSelection } from '../views/elements/UseCaseSelection';
|
||||
import { ValidatedServerConfig } from '../../utils/ValidatedServerConfig';
|
||||
import { isLocalRoom } from '../../utils/localRoom/isLocalRoom';
|
||||
import { SdkContextClass, SDKContext } from '../../contexts/SDKContext';
|
||||
import { viewUserDeviceSettings } from '../../actions/handlers/viewUserDeviceSettings';
|
||||
import { isNumberArray } from '../../utils/TypeUtils';
|
||||
import { VoiceBroadcastResumer } from '../../voice-broadcast';
|
||||
|
||||
// legacy export
|
||||
export { default as Views } from "../../Views";
|
||||
|
@ -202,7 +203,7 @@ interface IState {
|
|||
// When showing Modal dialogs we need to set aria-hidden on the root app element
|
||||
// and disable it when there are no dialogs
|
||||
hideToSRUsers: boolean;
|
||||
syncError?: MatrixError;
|
||||
syncError?: Error;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
serverConfig?: ValidatedServerConfig;
|
||||
ready: boolean;
|
||||
|
@ -234,14 +235,18 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
private focusComposer: boolean;
|
||||
private subTitleStatus: string;
|
||||
private prevWindowWidth: number;
|
||||
private voiceBroadcastResumer: VoiceBroadcastResumer;
|
||||
|
||||
private readonly loggedInView: React.RefObject<LoggedInViewType>;
|
||||
private readonly dispatcherRef: string;
|
||||
private readonly themeWatcher: ThemeWatcher;
|
||||
private readonly fontWatcher: FontWatcher;
|
||||
private readonly stores: SdkContextClass;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.stores = SdkContextClass.instance;
|
||||
this.stores.constructEagerStores();
|
||||
|
||||
this.state = {
|
||||
view: Views.LOADING,
|
||||
|
@ -430,6 +435,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
window.removeEventListener("resize", this.onWindowResized);
|
||||
|
||||
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
|
||||
if (this.voiceBroadcastResumer) this.voiceBroadcastResumer.destroy();
|
||||
}
|
||||
|
||||
private onWindowResized = (): void => {
|
||||
|
@ -763,6 +769,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
Modal.createDialog(DialPadModal, {}, "mx_Dialog_dialPadWrapper");
|
||||
break;
|
||||
case Action.OnLoggedIn:
|
||||
this.stores.client = MatrixClientPeg.get();
|
||||
if (
|
||||
// Skip this handling for token login as that always calls onLoggedIn itself
|
||||
!this.tokenLogin &&
|
||||
|
@ -1473,7 +1480,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
if (data.error instanceof InvalidStoreError) {
|
||||
Lifecycle.handleInvalidStoreError(data.error);
|
||||
}
|
||||
this.setState({ syncError: data.error || {} as MatrixError });
|
||||
this.setState({ syncError: data.error });
|
||||
} else if (this.state.syncError) {
|
||||
this.setState({ syncError: null });
|
||||
}
|
||||
|
@ -1637,6 +1644,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.voiceBroadcastResumer = new VoiceBroadcastResumer(cli);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2111,7 +2120,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
return <ErrorBoundary>
|
||||
{ view }
|
||||
<SDKContext.Provider value={this.stores}>
|
||||
{ view }
|
||||
</SDKContext.Provider>
|
||||
</ErrorBoundary>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,21 +44,18 @@ import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
|
|||
import ResizeNotifier from '../../utils/ResizeNotifier';
|
||||
import ContentMessages from '../../ContentMessages';
|
||||
import Modal from '../../Modal';
|
||||
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../../LegacyCallHandler';
|
||||
import { LegacyCallHandlerEvent } from '../../LegacyCallHandler';
|
||||
import dis, { defaultDispatcher } from '../../dispatcher/dispatcher';
|
||||
import * as Rooms from '../../Rooms';
|
||||
import eventSearch, { searchPagination } from '../../Searching';
|
||||
import MainSplit from './MainSplit';
|
||||
import RightPanel from './RightPanel';
|
||||
import { RoomViewStore } from '../../stores/RoomViewStore';
|
||||
import RoomScrollStateStore, { ScrollState } from '../../stores/RoomScrollStateStore';
|
||||
import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import { Layout } from "../../settings/enums/Layout";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RightPanelStore from "../../stores/right-panel/RightPanelStore";
|
||||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
import MatrixClientContext, { MatrixClientProps, withMatrixClientHOC } from "../../contexts/MatrixClientContext";
|
||||
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||
|
@ -76,12 +73,10 @@ import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
|||
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
||||
import { containsEmoji } from '../../effects/utils';
|
||||
import { CHAT_EFFECTS } from '../../effects';
|
||||
import WidgetStore from "../../stores/WidgetStore";
|
||||
import { CallView } from "../views/voip/CallView";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import Notifier from "../../Notifier";
|
||||
import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
|
||||
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
|
||||
import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
||||
import { getKeyBindingsManager } from '../../KeyBindingsManager';
|
||||
import { objectHasDiff } from "../../utils/objects";
|
||||
|
@ -118,8 +113,8 @@ import { isLocalRoom } from '../../utils/localRoom/isLocalRoom';
|
|||
import { ShowThreadPayload } from "../../dispatcher/payloads/ShowThreadPayload";
|
||||
import { RoomStatusBarUnsentMessages } from './RoomStatusBarUnsentMessages';
|
||||
import { LargeLoader } from './LargeLoader';
|
||||
import { VoiceBroadcastInfoEventType } from '../../voice-broadcast';
|
||||
import { isVideoRoom } from '../../utils/video-rooms';
|
||||
import { SDKContext } from '../../contexts/SDKContext';
|
||||
import { CallStore, CallStoreEvent } from "../../stores/CallStore";
|
||||
import { Call } from "../../models/Call";
|
||||
|
||||
|
@ -133,7 +128,7 @@ if (DEBUG) {
|
|||
debuglog = logger.log.bind(console);
|
||||
}
|
||||
|
||||
interface IRoomProps extends MatrixClientProps {
|
||||
interface IRoomProps {
|
||||
threepidInvite: IThreepidInvite;
|
||||
oobData?: IOOBData;
|
||||
|
||||
|
@ -203,7 +198,6 @@ export interface IRoomState {
|
|||
upgradeRecommendation?: IRecommendedVersion;
|
||||
canReact: boolean;
|
||||
canSendMessages: boolean;
|
||||
canSendVoiceBroadcasts: boolean;
|
||||
tombstone?: MatrixEvent;
|
||||
resizing: boolean;
|
||||
layout: Layout;
|
||||
|
@ -381,13 +375,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
private messagePanel: TimelinePanel;
|
||||
private roomViewBody = createRef<HTMLDivElement>();
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
public context!: React.ContextType<typeof MatrixClientContext>;
|
||||
static contextType = SDKContext;
|
||||
public context!: React.ContextType<typeof SDKContext>;
|
||||
|
||||
constructor(props: IRoomProps, context: React.ContextType<typeof MatrixClientContext>) {
|
||||
constructor(props: IRoomProps, context: React.ContextType<typeof SDKContext>) {
|
||||
super(props, context);
|
||||
|
||||
const llMembers = context.hasLazyLoadMembersEnabled();
|
||||
const llMembers = context.client.hasLazyLoadMembersEnabled();
|
||||
this.state = {
|
||||
roomId: null,
|
||||
roomLoading: true,
|
||||
|
@ -408,7 +402,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
statusBarVisible: false,
|
||||
canReact: false,
|
||||
canSendMessages: false,
|
||||
canSendVoiceBroadcasts: false,
|
||||
resizing: false,
|
||||
layout: SettingsStore.getValue("layout"),
|
||||
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
|
||||
|
@ -422,7 +415,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
showJoinLeaves: true,
|
||||
showAvatarChanges: true,
|
||||
showDisplaynameChanges: true,
|
||||
matrixClientIsReady: context?.isInitialSyncComplete(),
|
||||
matrixClientIsReady: context.client?.isInitialSyncComplete(),
|
||||
mainSplitContentType: MainSplitContentType.Timeline,
|
||||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
liveTimeline: undefined,
|
||||
|
@ -430,25 +423,25 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
};
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
context.on(ClientEvent.Room, this.onRoom);
|
||||
context.on(RoomEvent.Timeline, this.onRoomTimeline);
|
||||
context.on(RoomEvent.TimelineReset, this.onRoomTimelineReset);
|
||||
context.on(RoomEvent.Name, this.onRoomName);
|
||||
context.on(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
context.on(RoomStateEvent.Update, this.onRoomStateUpdate);
|
||||
context.on(RoomEvent.MyMembership, this.onMyMembership);
|
||||
context.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
|
||||
context.on(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
|
||||
context.on(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged);
|
||||
context.on(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged);
|
||||
context.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||
context.client.on(ClientEvent.Room, this.onRoom);
|
||||
context.client.on(RoomEvent.Timeline, this.onRoomTimeline);
|
||||
context.client.on(RoomEvent.TimelineReset, this.onRoomTimelineReset);
|
||||
context.client.on(RoomEvent.Name, this.onRoomName);
|
||||
context.client.on(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
context.client.on(RoomStateEvent.Update, this.onRoomStateUpdate);
|
||||
context.client.on(RoomEvent.MyMembership, this.onMyMembership);
|
||||
context.client.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
|
||||
context.client.on(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
|
||||
context.client.on(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged);
|
||||
context.client.on(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged);
|
||||
context.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||
// Start listening for RoomViewStore updates
|
||||
RoomViewStore.instance.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
context.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
|
||||
RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
context.rightPanelStore.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
|
||||
WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
|
||||
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
||||
context.widgetStore.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
||||
|
||||
CallStore.instance.on(CallStoreEvent.ActiveCalls, this.onActiveCalls);
|
||||
|
||||
|
@ -501,16 +494,16 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
action: "appsDrawer",
|
||||
show: true,
|
||||
});
|
||||
if (WidgetLayoutStore.instance.hasMaximisedWidget(this.state.room)) {
|
||||
if (this.context.widgetLayoutStore.hasMaximisedWidget(this.state.room)) {
|
||||
// Show chat in right panel when a widget is maximised
|
||||
RightPanelStore.instance.setCard({ phase: RightPanelPhases.Timeline });
|
||||
this.context.rightPanelStore.setCard({ phase: RightPanelPhases.Timeline });
|
||||
}
|
||||
this.checkWidgets(this.state.room);
|
||||
};
|
||||
|
||||
private checkWidgets = (room: Room): void => {
|
||||
this.setState({
|
||||
hasPinnedWidgets: WidgetLayoutStore.instance.hasPinnedWidgets(room),
|
||||
hasPinnedWidgets: this.context.widgetLayoutStore.hasPinnedWidgets(room),
|
||||
mainSplitContentType: this.getMainSplitContentType(room),
|
||||
showApps: this.shouldShowApps(room),
|
||||
});
|
||||
|
@ -518,12 +511,12 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
private getMainSplitContentType = (room: Room) => {
|
||||
if (
|
||||
(SettingsStore.getValue("feature_group_calls") && RoomViewStore.instance.isViewingCall())
|
||||
(SettingsStore.getValue("feature_group_calls") && this.context.roomViewStore.isViewingCall())
|
||||
|| isVideoRoom(room)
|
||||
) {
|
||||
return MainSplitContentType.Call;
|
||||
}
|
||||
if (WidgetLayoutStore.instance.hasMaximisedWidget(room)) {
|
||||
if (this.context.widgetLayoutStore.hasMaximisedWidget(room)) {
|
||||
return MainSplitContentType.MaximisedWidget;
|
||||
}
|
||||
return MainSplitContentType.Timeline;
|
||||
|
@ -534,7 +527,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!initial && this.state.roomId !== RoomViewStore.instance.getRoomId()) {
|
||||
if (!initial && this.state.roomId !== this.context.roomViewStore.getRoomId()) {
|
||||
// RoomView explicitly does not support changing what room
|
||||
// is being viewed: instead it should just be re-mounted when
|
||||
// switching rooms. Therefore, if the room ID changes, we
|
||||
|
@ -549,45 +542,45 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
return;
|
||||
}
|
||||
|
||||
const roomId = RoomViewStore.instance.getRoomId();
|
||||
const room = this.context.getRoom(roomId);
|
||||
const roomId = this.context.roomViewStore.getRoomId();
|
||||
const room = this.context.client.getRoom(roomId);
|
||||
|
||||
// This convoluted type signature ensures we get IntelliSense *and* correct typing
|
||||
const newState: Partial<IRoomState> & Pick<IRoomState, any> = {
|
||||
roomId,
|
||||
roomAlias: RoomViewStore.instance.getRoomAlias(),
|
||||
roomLoading: RoomViewStore.instance.isRoomLoading(),
|
||||
roomLoadError: RoomViewStore.instance.getRoomLoadError(),
|
||||
joining: RoomViewStore.instance.isJoining(),
|
||||
replyToEvent: RoomViewStore.instance.getQuotingEvent(),
|
||||
roomAlias: this.context.roomViewStore.getRoomAlias(),
|
||||
roomLoading: this.context.roomViewStore.isRoomLoading(),
|
||||
roomLoadError: this.context.roomViewStore.getRoomLoadError(),
|
||||
joining: this.context.roomViewStore.isJoining(),
|
||||
replyToEvent: this.context.roomViewStore.getQuotingEvent(),
|
||||
// we should only peek once we have a ready client
|
||||
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.instance.shouldPeek(),
|
||||
shouldPeek: this.state.matrixClientIsReady && this.context.roomViewStore.shouldPeek(),
|
||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
||||
showRedactions: SettingsStore.getValue("showRedactions", roomId),
|
||||
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
|
||||
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
|
||||
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
|
||||
wasContextSwitch: RoomViewStore.instance.getWasContextSwitch(),
|
||||
wasContextSwitch: this.context.roomViewStore.getWasContextSwitch(),
|
||||
mainSplitContentType: room === null ? undefined : this.getMainSplitContentType(room),
|
||||
initialEventId: null, // default to clearing this, will get set later in the method if needed
|
||||
showRightPanel: RightPanelStore.instance.isOpenForRoom(roomId),
|
||||
showRightPanel: this.context.rightPanelStore.isOpenForRoom(roomId),
|
||||
activeCall: CallStore.instance.getActiveCall(roomId),
|
||||
};
|
||||
|
||||
if (
|
||||
this.state.mainSplitContentType !== MainSplitContentType.Timeline
|
||||
&& newState.mainSplitContentType === MainSplitContentType.Timeline
|
||||
&& RightPanelStore.instance.isOpen
|
||||
&& RightPanelStore.instance.currentCard.phase === RightPanelPhases.Timeline
|
||||
&& RightPanelStore.instance.roomPhaseHistory.some(card => (card.phase === RightPanelPhases.Timeline))
|
||||
&& this.context.rightPanelStore.isOpen
|
||||
&& this.context.rightPanelStore.currentCard.phase === RightPanelPhases.Timeline
|
||||
&& this.context.rightPanelStore.roomPhaseHistory.some(card => (card.phase === RightPanelPhases.Timeline))
|
||||
) {
|
||||
// We're returning to the main timeline, so hide the right panel timeline
|
||||
RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomSummary });
|
||||
RightPanelStore.instance.togglePanel(this.state.roomId ?? null);
|
||||
this.context.rightPanelStore.setCard({ phase: RightPanelPhases.RoomSummary });
|
||||
this.context.rightPanelStore.togglePanel(this.state.roomId ?? null);
|
||||
newState.showRightPanel = false;
|
||||
}
|
||||
|
||||
const initialEventId = RoomViewStore.instance.getInitialEventId();
|
||||
const initialEventId = this.context.roomViewStore.getInitialEventId();
|
||||
if (initialEventId) {
|
||||
let initialEvent = room?.findEventById(initialEventId);
|
||||
// The event does not exist in the current sync data
|
||||
|
@ -600,7 +593,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
// becomes available to fetch a whole thread
|
||||
if (!initialEvent) {
|
||||
initialEvent = await fetchInitialEvent(
|
||||
this.context,
|
||||
this.context.client,
|
||||
roomId,
|
||||
initialEventId,
|
||||
);
|
||||
|
@ -616,21 +609,21 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
action: Action.ShowThread,
|
||||
rootEvent: thread.rootEvent,
|
||||
initialEvent,
|
||||
highlighted: RoomViewStore.instance.isInitialEventHighlighted(),
|
||||
scroll_into_view: RoomViewStore.instance.initialEventScrollIntoView(),
|
||||
highlighted: this.context.roomViewStore.isInitialEventHighlighted(),
|
||||
scroll_into_view: this.context.roomViewStore.initialEventScrollIntoView(),
|
||||
});
|
||||
} else {
|
||||
newState.initialEventId = initialEventId;
|
||||
newState.isInitialEventHighlighted = RoomViewStore.instance.isInitialEventHighlighted();
|
||||
newState.initialEventScrollIntoView = RoomViewStore.instance.initialEventScrollIntoView();
|
||||
newState.isInitialEventHighlighted = this.context.roomViewStore.isInitialEventHighlighted();
|
||||
newState.initialEventScrollIntoView = this.context.roomViewStore.initialEventScrollIntoView();
|
||||
|
||||
if (thread && initialEvent?.isThreadRoot) {
|
||||
dis.dispatch<ShowThreadPayload>({
|
||||
action: Action.ShowThread,
|
||||
rootEvent: thread.rootEvent,
|
||||
initialEvent,
|
||||
highlighted: RoomViewStore.instance.isInitialEventHighlighted(),
|
||||
scroll_into_view: RoomViewStore.instance.initialEventScrollIntoView(),
|
||||
highlighted: this.context.roomViewStore.isInitialEventHighlighted(),
|
||||
scroll_into_view: this.context.roomViewStore.initialEventScrollIntoView(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -657,7 +650,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
|
||||
// Stop peeking because we have joined this room now
|
||||
this.context.stopPeeking();
|
||||
this.context.client.stopPeeking();
|
||||
}
|
||||
|
||||
// Temporary logging to diagnose https://github.com/vector-im/element-web/issues/4307
|
||||
|
@ -674,7 +667,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
// NB: This does assume that the roomID will not change for the lifetime of
|
||||
// the RoomView instance
|
||||
if (initial) {
|
||||
newState.room = this.context.getRoom(newState.roomId);
|
||||
newState.room = this.context.client.getRoom(newState.roomId);
|
||||
if (newState.room) {
|
||||
newState.showApps = this.shouldShowApps(newState.room);
|
||||
this.onRoomLoaded(newState.room);
|
||||
|
@ -784,7 +777,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
peekLoading: true,
|
||||
isPeeking: true, // this will change to false if peeking fails
|
||||
});
|
||||
this.context.peekInRoom(roomId).then((room) => {
|
||||
this.context.client.peekInRoom(roomId).then((room) => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
|
@ -817,7 +810,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
});
|
||||
} else if (room) {
|
||||
// Stop peeking because we have joined this room previously
|
||||
this.context.stopPeeking();
|
||||
this.context.client.stopPeeking();
|
||||
this.setState({ isPeeking: false });
|
||||
}
|
||||
}
|
||||
|
@ -835,7 +828,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
// Otherwise (in case the user set hideWidgetDrawer by clicking the button) follow the parameter.
|
||||
const isManuallyShown = hideWidgetDrawer ? hideWidgetDrawer === "false": true;
|
||||
|
||||
const widgets = WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top);
|
||||
const widgets = this.context.widgetLayoutStore.getContainerWidgets(room, Container.Top);
|
||||
return isManuallyShown && widgets.length > 0;
|
||||
}
|
||||
|
||||
|
@ -848,7 +841,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
callState: callState,
|
||||
});
|
||||
|
||||
LegacyCallHandler.instance.on(LegacyCallHandlerEvent.CallState, this.onCallState);
|
||||
this.context.legacyCallHandler.on(LegacyCallHandlerEvent.CallState, this.onCallState);
|
||||
window.addEventListener('beforeunload', this.onPageUnload);
|
||||
}
|
||||
|
||||
|
@ -885,7 +878,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
// (We could use isMounted, but facebook have deprecated that.)
|
||||
this.unmounted = true;
|
||||
|
||||
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.onCallState);
|
||||
this.context.legacyCallHandler.removeListener(LegacyCallHandlerEvent.CallState, this.onCallState);
|
||||
|
||||
// update the scroll map before we get unmounted
|
||||
if (this.state.roomId) {
|
||||
|
@ -893,47 +886,47 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
}
|
||||
|
||||
if (this.state.shouldPeek) {
|
||||
this.context.stopPeeking();
|
||||
this.context.client.stopPeeking();
|
||||
}
|
||||
|
||||
// stop tracking room changes to format permalinks
|
||||
this.stopAllPermalinkCreators();
|
||||
|
||||
dis.unregister(this.dispatcherRef);
|
||||
if (this.context) {
|
||||
this.context.removeListener(ClientEvent.Room, this.onRoom);
|
||||
this.context.removeListener(RoomEvent.Timeline, this.onRoomTimeline);
|
||||
this.context.removeListener(RoomEvent.TimelineReset, this.onRoomTimelineReset);
|
||||
this.context.removeListener(RoomEvent.Name, this.onRoomName);
|
||||
this.context.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
this.context.removeListener(RoomEvent.MyMembership, this.onMyMembership);
|
||||
this.context.removeListener(RoomStateEvent.Update, this.onRoomStateUpdate);
|
||||
this.context.removeListener(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
|
||||
this.context.removeListener(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
|
||||
this.context.removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged);
|
||||
this.context.removeListener(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged);
|
||||
this.context.removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||
if (this.context.client) {
|
||||
this.context.client.removeListener(ClientEvent.Room, this.onRoom);
|
||||
this.context.client.removeListener(RoomEvent.Timeline, this.onRoomTimeline);
|
||||
this.context.client.removeListener(RoomEvent.TimelineReset, this.onRoomTimelineReset);
|
||||
this.context.client.removeListener(RoomEvent.Name, this.onRoomName);
|
||||
this.context.client.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
this.context.client.removeListener(RoomEvent.MyMembership, this.onMyMembership);
|
||||
this.context.client.removeListener(RoomStateEvent.Update, this.onRoomStateUpdate);
|
||||
this.context.client.removeListener(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
|
||||
this.context.client.removeListener(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
|
||||
this.context.client.removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged);
|
||||
this.context.client.removeListener(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged);
|
||||
this.context.client.removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||
}
|
||||
|
||||
window.removeEventListener('beforeunload', this.onPageUnload);
|
||||
|
||||
RoomViewStore.instance.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
this.context.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
|
||||
RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
this.context.rightPanelStore.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
WidgetEchoStore.removeListener(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
|
||||
WidgetStore.instance.removeListener(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
||||
this.context.widgetStore.removeListener(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
||||
|
||||
this.props.resizeNotifier.off("isResizing", this.onIsResizing);
|
||||
|
||||
if (this.state.room) {
|
||||
WidgetLayoutStore.instance.off(
|
||||
this.context.widgetLayoutStore.off(
|
||||
WidgetLayoutStore.emissionForRoom(this.state.room),
|
||||
this.onWidgetLayoutChange,
|
||||
);
|
||||
}
|
||||
|
||||
CallStore.instance.off(CallStoreEvent.ActiveCalls, this.onActiveCalls);
|
||||
LegacyCallHandler.instance.off(LegacyCallHandlerEvent.CallState, this.onCallState);
|
||||
this.context.legacyCallHandler.off(LegacyCallHandlerEvent.CallState, this.onCallState);
|
||||
|
||||
// cancel any pending calls to the throttled updated
|
||||
this.updateRoomMembers.cancel();
|
||||
|
@ -944,13 +937,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
if (this.viewsLocalRoom) {
|
||||
// clean up if this was a local room
|
||||
this.props.mxClient.store.removeRoom(this.state.room.roomId);
|
||||
this.context.client.store.removeRoom(this.state.room.roomId);
|
||||
}
|
||||
}
|
||||
|
||||
private onRightPanelStoreUpdate = () => {
|
||||
this.setState({
|
||||
showRightPanel: RightPanelStore.instance.isOpenForRoom(this.state.roomId),
|
||||
showRightPanel: this.context.rightPanelStore.isOpenForRoom(this.state.roomId),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1017,7 +1010,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
break;
|
||||
case 'picture_snapshot':
|
||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
[payload.file], this.state.room.roomId, null, this.context);
|
||||
[payload.file], this.state.room.roomId, null, this.context.client);
|
||||
break;
|
||||
case 'notifier_enabled':
|
||||
case Action.UploadStarted:
|
||||
|
@ -1043,7 +1036,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
case 'MatrixActions.sync':
|
||||
if (!this.state.matrixClientIsReady) {
|
||||
this.setState({
|
||||
matrixClientIsReady: this.context?.isInitialSyncComplete(),
|
||||
matrixClientIsReady: this.context.client?.isInitialSyncComplete(),
|
||||
}, () => {
|
||||
// send another "initial" RVS update to trigger peeking if needed
|
||||
this.onRoomViewStoreUpdate(true);
|
||||
|
@ -1112,7 +1105,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
private onLocalRoomEvent(roomId: string) {
|
||||
if (roomId !== this.state.room.roomId) return;
|
||||
createRoomFromLocalRoom(this.props.mxClient, this.state.room as LocalRoom);
|
||||
createRoomFromLocalRoom(this.context.client, this.state.room as LocalRoom);
|
||||
}
|
||||
|
||||
private onRoomTimeline = (ev: MatrixEvent, room: Room | null, toStartOfTimeline: boolean, removed, data) => {
|
||||
|
@ -1145,7 +1138,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
this.handleEffects(ev);
|
||||
}
|
||||
|
||||
if (ev.getSender() !== this.context.credentials.userId) {
|
||||
if (ev.getSender() !== this.context.client.credentials.userId) {
|
||||
// update unread count when scrolled up
|
||||
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
|
||||
// no change
|
||||
|
@ -1165,7 +1158,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
};
|
||||
|
||||
private handleEffects = (ev: MatrixEvent) => {
|
||||
const notifState = RoomNotificationStateStore.instance.getRoomState(this.state.room);
|
||||
const notifState = this.context.roomNotificationStateStore.getRoomState(this.state.room);
|
||||
if (!notifState.isUnread) return;
|
||||
|
||||
CHAT_EFFECTS.forEach(effect => {
|
||||
|
@ -1202,7 +1195,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
private onRoomLoaded = (room: Room) => {
|
||||
if (this.unmounted) return;
|
||||
// Attach a widget store listener only when we get a room
|
||||
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange);
|
||||
this.context.widgetLayoutStore.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange);
|
||||
|
||||
this.calculatePeekRules(room);
|
||||
this.updatePreviewUrlVisibility(room);
|
||||
|
@ -1214,10 +1207,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
if (
|
||||
this.getMainSplitContentType(room) !== MainSplitContentType.Timeline
|
||||
&& RoomNotificationStateStore.instance.getRoomState(room).isUnread
|
||||
&& this.context.roomNotificationStateStore.getRoomState(room).isUnread
|
||||
) {
|
||||
// Automatically open the chat panel to make unread messages easier to discover
|
||||
RightPanelStore.instance.setCard({ phase: RightPanelPhases.Timeline }, true, room.roomId);
|
||||
this.context.rightPanelStore.setCard({ phase: RightPanelPhases.Timeline }, true, room.roomId);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
|
@ -1244,7 +1237,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
private async loadMembersIfJoined(room: Room) {
|
||||
// lazy load members if enabled
|
||||
if (this.context.hasLazyLoadMembersEnabled()) {
|
||||
if (this.context.client.hasLazyLoadMembersEnabled()) {
|
||||
if (room && room.getMyMembership() === 'join') {
|
||||
try {
|
||||
await room.loadMembersIfNeeded();
|
||||
|
@ -1270,7 +1263,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
private updatePreviewUrlVisibility({ roomId }: Room) {
|
||||
// URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit
|
||||
const key = this.context.isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled';
|
||||
const key = this.context.client.isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled';
|
||||
this.setState({
|
||||
showUrlPreview: SettingsStore.getValue(key, roomId),
|
||||
});
|
||||
|
@ -1283,7 +1276,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
// Detach the listener if the room is changing for some reason
|
||||
if (this.state.room) {
|
||||
WidgetLayoutStore.instance.off(
|
||||
this.context.widgetLayoutStore.off(
|
||||
WidgetLayoutStore.emissionForRoom(this.state.room),
|
||||
this.onWidgetLayoutChange,
|
||||
);
|
||||
|
@ -1320,15 +1313,15 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
};
|
||||
|
||||
private async updateE2EStatus(room: Room) {
|
||||
if (!this.context.isRoomEncrypted(room.roomId)) return;
|
||||
if (!this.context.client.isRoomEncrypted(room.roomId)) return;
|
||||
|
||||
// If crypto is not currently enabled, we aren't tracking devices at all,
|
||||
// so we don't know what the answer is. Let's error on the safe side and show
|
||||
// a warning for this case.
|
||||
let e2eStatus = E2EStatus.Warning;
|
||||
if (this.context.isCryptoEnabled()) {
|
||||
if (this.context.client.isCryptoEnabled()) {
|
||||
/* At this point, the user has encryption on and cross-signing on */
|
||||
e2eStatus = await shieldStatusForRoom(this.context, room);
|
||||
e2eStatus = await shieldStatusForRoom(this.context.client, room);
|
||||
}
|
||||
|
||||
if (this.unmounted) return;
|
||||
|
@ -1374,19 +1367,17 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
private updatePermissions(room: Room) {
|
||||
if (room) {
|
||||
const me = this.context.getUserId();
|
||||
const me = this.context.client.getUserId();
|
||||
const canReact = (
|
||||
room.getMyMembership() === "join" &&
|
||||
room.currentState.maySendEvent(EventType.Reaction, me)
|
||||
);
|
||||
const canSendMessages = room.maySendMessage();
|
||||
const canSelfRedact = room.currentState.maySendEvent(EventType.RoomRedaction, me);
|
||||
const canSendVoiceBroadcasts = room.currentState.maySendEvent(VoiceBroadcastInfoEventType, me);
|
||||
|
||||
this.setState({
|
||||
canReact,
|
||||
canSendMessages,
|
||||
canSendVoiceBroadcasts,
|
||||
canSelfRedact,
|
||||
});
|
||||
}
|
||||
|
@ -1442,7 +1433,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
private onJoinButtonClicked = () => {
|
||||
// If the user is a ROU, allow them to transition to a PWLU
|
||||
if (this.context?.isGuest()) {
|
||||
if (this.context.client?.isGuest()) {
|
||||
// Join this room once the user has registered and logged in
|
||||
// (If we failed to peek, we may not have a valid room object.)
|
||||
dis.dispatch<DoAfterSyncPreparedPayload<ViewRoomPayload>>({
|
||||
|
@ -1499,13 +1490,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
};
|
||||
|
||||
private injectSticker(url: string, info: object, text: string, threadId: string | null) {
|
||||
if (this.context.isGuest()) {
|
||||
if (this.context.client.isGuest()) {
|
||||
dis.dispatch({ action: 'require_registration' });
|
||||
return;
|
||||
}
|
||||
|
||||
ContentMessages.sharedInstance()
|
||||
.sendStickerContentToRoom(url, this.state.room.roomId, threadId, info, text, this.context)
|
||||
.sendStickerContentToRoom(url, this.state.room.roomId, threadId, info, text, this.context.client)
|
||||
.then(undefined, (error) => {
|
||||
if (error.name === "UnknownDeviceError") {
|
||||
// Let the staus bar handle this
|
||||
|
@ -1578,7 +1569,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
return b.length - a.length;
|
||||
});
|
||||
|
||||
if (this.context.supportsExperimentalThreads()) {
|
||||
if (this.context.client.supportsExperimentalThreads()) {
|
||||
// Process all thread roots returned in this batch of search results
|
||||
// XXX: This won't work for results coming from Seshat which won't include the bundled relationship
|
||||
for (const result of results.results) {
|
||||
|
@ -1586,7 +1577,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
const bundledRelationship = event
|
||||
.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
|
||||
if (!bundledRelationship || event.getThread()) continue;
|
||||
const room = this.context.getRoom(event.getRoomId());
|
||||
const room = this.context.client.getRoom(event.getRoomId());
|
||||
const thread = room.findThreadForEvent(event);
|
||||
if (thread) {
|
||||
event.setThread(thread);
|
||||
|
@ -1658,7 +1649,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
const mxEv = result.context.getEvent();
|
||||
const roomId = mxEv.getRoomId();
|
||||
const room = this.context.getRoom(roomId);
|
||||
const room = this.context.client.getRoom(roomId);
|
||||
if (!room) {
|
||||
// if we do not have the room in js-sdk stores then hide it as we cannot easily show it
|
||||
// As per the spec, an all rooms search can create this condition,
|
||||
|
@ -1715,7 +1706,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
this.setState({
|
||||
rejecting: true,
|
||||
});
|
||||
this.context.leave(this.state.roomId).then(() => {
|
||||
this.context.client.leave(this.state.roomId).then(() => {
|
||||
dis.dispatch({ action: Action.ViewHomePage });
|
||||
this.setState({
|
||||
rejecting: false,
|
||||
|
@ -1742,13 +1733,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
});
|
||||
|
||||
try {
|
||||
const myMember = this.state.room.getMember(this.context.getUserId());
|
||||
const myMember = this.state.room.getMember(this.context.client.getUserId());
|
||||
const inviteEvent = myMember.events.member;
|
||||
const ignoredUsers = this.context.getIgnoredUsers();
|
||||
const ignoredUsers = this.context.client.getIgnoredUsers();
|
||||
ignoredUsers.push(inviteEvent.getSender()); // de-duped internally in the js-sdk
|
||||
await this.context.setIgnoredUsers(ignoredUsers);
|
||||
await this.context.client.setIgnoredUsers(ignoredUsers);
|
||||
|
||||
await this.context.leave(this.state.roomId);
|
||||
await this.context.client.leave(this.state.roomId);
|
||||
dis.dispatch({ action: Action.ViewHomePage });
|
||||
this.setState({
|
||||
rejecting: false,
|
||||
|
@ -1911,7 +1902,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
if (!this.state.room) {
|
||||
return null;
|
||||
}
|
||||
return LegacyCallHandler.instance.getCallForRoom(this.state.room.roomId);
|
||||
return this.context.legacyCallHandler.getCallForRoom(this.state.room.roomId);
|
||||
}
|
||||
|
||||
// this has to be a proper method rather than an unnamed function,
|
||||
|
@ -1924,7 +1915,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
const createEvent = this.state.room.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||
if (!createEvent || !createEvent.getContent()['predecessor']) return null;
|
||||
|
||||
return this.context.getRoom(createEvent.getContent()['predecessor']['room_id']);
|
||||
return this.context.client.getRoom(createEvent.getContent()['predecessor']['room_id']);
|
||||
}
|
||||
|
||||
getHiddenHighlightCount() {
|
||||
|
@ -1953,7 +1944,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
Array.from(dataTransfer.files),
|
||||
this.state.room?.roomId ?? this.state.roomId,
|
||||
null,
|
||||
this.context,
|
||||
this.context.client,
|
||||
TimelineRenderingType.Room,
|
||||
);
|
||||
|
||||
|
@ -1970,7 +1961,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
}
|
||||
|
||||
private renderLocalRoomCreateLoader(): ReactElement {
|
||||
const names = this.state.room.getDefaultRoomName(this.props.mxClient.getUserId());
|
||||
const names = this.state.room.getDefaultRoomName(this.context.client.getUserId());
|
||||
return <RoomContext.Provider value={this.state}>
|
||||
<LocalRoomCreateLoader
|
||||
names={names}
|
||||
|
@ -2081,7 +2072,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
</ErrorBoundary>
|
||||
);
|
||||
} else {
|
||||
const myUserId = this.context.credentials.userId;
|
||||
const myUserId = this.context.client.credentials.userId;
|
||||
const myMember = this.state.room.getMember(myUserId);
|
||||
const inviteEvent = myMember ? myMember.events.member : null;
|
||||
let inviterName = _t("Unknown");
|
||||
|
@ -2162,7 +2153,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
const showRoomUpgradeBar = (
|
||||
roomVersionRecommendation &&
|
||||
roomVersionRecommendation.needsUpgrade &&
|
||||
this.state.room.userMayUpgradeRoom(this.context.credentials.userId)
|
||||
this.state.room.userMayUpgradeRoom(this.context.client.credentials.userId)
|
||||
);
|
||||
|
||||
const hiddenHighlightCount = this.getHiddenHighlightCount();
|
||||
|
@ -2174,7 +2165,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
searchInProgress={this.state.searchInProgress}
|
||||
onCancelClick={this.onCancelSearchClick}
|
||||
onSearch={this.onSearch}
|
||||
isRoomEncrypted={this.context.isRoomEncrypted(this.state.room.roomId)}
|
||||
isRoomEncrypted={this.context.client.isRoomEncrypted(this.state.room.roomId)}
|
||||
/>;
|
||||
} else if (showRoomUpgradeBar) {
|
||||
aux = <RoomUpgradeWarningBar room={this.state.room} />;
|
||||
|
@ -2236,7 +2227,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
const auxPanel = (
|
||||
<AuxPanel
|
||||
room={this.state.room}
|
||||
userId={this.context.credentials.userId}
|
||||
userId={this.context.client.credentials.userId}
|
||||
showApps={this.state.showApps}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
|
@ -2257,7 +2248,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
resizeNotifier={this.props.resizeNotifier}
|
||||
replyToEvent={this.state.replyToEvent}
|
||||
permalinkCreator={this.permalinkCreator}
|
||||
showVoiceBroadcastButton={this.state.canSendVoiceBroadcasts}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
@ -2397,7 +2387,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
mainSplitBody = <>
|
||||
<AppsDrawer
|
||||
room={this.state.room}
|
||||
userId={this.context.credentials.userId}
|
||||
userId={this.context.client.credentials.userId}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
showApps={true}
|
||||
/>
|
||||
|
@ -2451,7 +2441,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
onAppsClick = null;
|
||||
onForgetClick = null;
|
||||
onSearchClick = null;
|
||||
if (this.state.room.canInvite(this.context.credentials.userId)) {
|
||||
if (this.state.room.canInvite(this.context.client.credentials.userId)) {
|
||||
onInviteClick = this.onInviteClick;
|
||||
}
|
||||
viewingCall = true;
|
||||
|
@ -2493,5 +2483,4 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
}
|
||||
}
|
||||
|
||||
const RoomViewWithMatrixClient = withMatrixClientHOC(RoomView);
|
||||
export default RoomViewWithMatrixClient;
|
||||
export default RoomView;
|
||||
|
|
|
@ -60,13 +60,13 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
|
|||
import { useTypedEventEmitterState } from "../../hooks/useEventEmitter";
|
||||
import { IOOBData } from "../../stores/ThreepidInviteStore";
|
||||
import { awaitRoomDownSync } from "../../utils/RoomUpgrade";
|
||||
import { RoomViewStore } from "../../stores/RoomViewStore";
|
||||
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { JoinRoomReadyPayload } from "../../dispatcher/payloads/JoinRoomReadyPayload";
|
||||
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
||||
import { getKeyBindingsManager } from "../../KeyBindingsManager";
|
||||
import { Alignment } from "../views/elements/Tooltip";
|
||||
import { getTopic } from "../../hooks/room/useTopic";
|
||||
import { SdkContextClass } from "../../contexts/SDKContext";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
|
@ -378,7 +378,7 @@ export const joinRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: st
|
|||
metricsTrigger: "SpaceHierarchy",
|
||||
});
|
||||
}, err => {
|
||||
RoomViewStore.instance.showJoinRoomError(err, roomId);
|
||||
SdkContextClass.instance.roomViewStore.showJoinRoomError(err, roomId);
|
||||
});
|
||||
|
||||
return prom;
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import React, { createRef, KeyboardEvent } from 'react';
|
||||
import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { Room, RoomEvent } from 'matrix-js-sdk/src/models/room';
|
||||
import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
|
||||
import { Direction } from 'matrix-js-sdk/src/models/event-timeline';
|
||||
|
@ -51,10 +51,10 @@ import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
|||
import Measured from '../views/elements/Measured';
|
||||
import PosthogTrackers from "../../PosthogTrackers";
|
||||
import { ButtonEvent } from "../views/elements/AccessibleButton";
|
||||
import { RoomViewStore } from '../../stores/RoomViewStore';
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import { ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import Heading from '../views/typography/Heading';
|
||||
import { SdkContextClass } from '../../contexts/SDKContext';
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -70,6 +70,7 @@ interface IProps {
|
|||
|
||||
interface IState {
|
||||
thread?: Thread;
|
||||
lastReply?: MatrixEvent | null;
|
||||
layout: Layout;
|
||||
editState?: EditorStateTransfer;
|
||||
replyToEvent?: MatrixEvent;
|
||||
|
@ -88,9 +89,16 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const thread = this.props.room.getThread(this.props.mxEvent.getId());
|
||||
|
||||
this.setupThreadListeners(thread);
|
||||
this.state = {
|
||||
layout: SettingsStore.getValue("layout"),
|
||||
narrow: false,
|
||||
thread,
|
||||
lastReply: thread?.lastReply((ev: MatrixEvent) => {
|
||||
return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status;
|
||||
}),
|
||||
};
|
||||
|
||||
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, (...[,,, value]) =>
|
||||
|
@ -99,6 +107,9 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
if (this.state.thread) {
|
||||
this.postThreadUpdate(this.state.thread);
|
||||
}
|
||||
this.setupThread(this.props.mxEvent);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
||||
|
@ -113,7 +124,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
room.removeListener(ThreadEvent.New, this.onNewThread);
|
||||
SettingsStore.unwatchSetting(this.layoutWatcherRef);
|
||||
|
||||
const hasRoomChanged = RoomViewStore.instance.getRoomId() !== roomId;
|
||||
const hasRoomChanged = SdkContextClass.instance.roomViewStore.getRoomId() !== roomId;
|
||||
if (this.props.isInitialEventHighlighted && !hasRoomChanged) {
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
|
@ -189,19 +200,49 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private updateThreadRelation = (): void => {
|
||||
this.setState({
|
||||
lastReply: this.threadLastReply,
|
||||
});
|
||||
};
|
||||
|
||||
private get threadLastReply(): MatrixEvent | undefined {
|
||||
return this.state.thread?.lastReply((ev: MatrixEvent) => {
|
||||
return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status;
|
||||
});
|
||||
}
|
||||
|
||||
private updateThread = (thread?: Thread) => {
|
||||
if (thread && this.state.thread !== thread) {
|
||||
if (this.state.thread === thread) return;
|
||||
|
||||
this.setupThreadListeners(thread, this.state.thread);
|
||||
if (thread) {
|
||||
this.setState({
|
||||
thread,
|
||||
}, async () => {
|
||||
thread.emit(ThreadEvent.ViewThread);
|
||||
await thread.fetchInitialEvents();
|
||||
this.nextBatch = thread.liveTimeline.getPaginationToken(Direction.Backward);
|
||||
this.timelinePanel.current?.refreshTimeline();
|
||||
});
|
||||
lastReply: this.threadLastReply,
|
||||
}, async () => this.postThreadUpdate(thread));
|
||||
}
|
||||
};
|
||||
|
||||
private async postThreadUpdate(thread: Thread): Promise<void> {
|
||||
thread.emit(ThreadEvent.ViewThread);
|
||||
await thread.fetchInitialEvents();
|
||||
this.updateThreadRelation();
|
||||
this.nextBatch = thread.liveTimeline.getPaginationToken(Direction.Backward);
|
||||
this.timelinePanel.current?.refreshTimeline();
|
||||
}
|
||||
|
||||
private setupThreadListeners(thread?: Thread | undefined, oldThread?: Thread | undefined): void {
|
||||
if (oldThread) {
|
||||
this.state.thread.off(ThreadEvent.NewReply, this.updateThreadRelation);
|
||||
this.props.room.off(RoomEvent.LocalEchoUpdated, this.updateThreadRelation);
|
||||
}
|
||||
if (thread) {
|
||||
thread.on(ThreadEvent.NewReply, this.updateThreadRelation);
|
||||
this.props.room.on(RoomEvent.LocalEchoUpdated, this.updateThreadRelation);
|
||||
}
|
||||
}
|
||||
|
||||
private resetJumpToEvent = (event?: string): void => {
|
||||
if (this.props.initialEvent && this.props.initialEventScrollIntoView &&
|
||||
event === this.props.initialEvent?.getId()) {
|
||||
|
@ -242,14 +283,14 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private nextBatch: string;
|
||||
private nextBatch: string | undefined | null = null;
|
||||
|
||||
private onPaginationRequest = async (
|
||||
timelineWindow: TimelineWindow | null,
|
||||
direction = Direction.Backward,
|
||||
limit = 20,
|
||||
): Promise<boolean> => {
|
||||
if (!Thread.hasServerSideSupport) {
|
||||
if (!Thread.hasServerSideSupport && timelineWindow) {
|
||||
timelineWindow.extend(direction, limit);
|
||||
return true;
|
||||
}
|
||||
|
@ -262,40 +303,50 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
opts.from = this.nextBatch;
|
||||
}
|
||||
|
||||
const { nextBatch } = await this.state.thread.fetchEvents(opts);
|
||||
|
||||
this.nextBatch = nextBatch;
|
||||
let nextBatch: string | null | undefined = null;
|
||||
if (this.state.thread) {
|
||||
const response = await this.state.thread.fetchEvents(opts);
|
||||
nextBatch = response.nextBatch;
|
||||
this.nextBatch = nextBatch;
|
||||
}
|
||||
|
||||
// Advances the marker on the TimelineWindow to define the correct
|
||||
// window of events to display on screen
|
||||
timelineWindow.extend(direction, limit);
|
||||
timelineWindow?.extend(direction, limit);
|
||||
|
||||
return !!nextBatch;
|
||||
};
|
||||
|
||||
private onFileDrop = (dataTransfer: DataTransfer) => {
|
||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
Array.from(dataTransfer.files),
|
||||
this.props.mxEvent.getRoomId(),
|
||||
this.threadRelation,
|
||||
MatrixClientPeg.get(),
|
||||
TimelineRenderingType.Thread,
|
||||
);
|
||||
const roomId = this.props.mxEvent.getRoomId();
|
||||
if (roomId) {
|
||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
Array.from(dataTransfer.files),
|
||||
roomId,
|
||||
this.threadRelation,
|
||||
MatrixClientPeg.get(),
|
||||
TimelineRenderingType.Thread,
|
||||
);
|
||||
} else {
|
||||
console.warn("Unknwon roomId for event", this.props.mxEvent);
|
||||
}
|
||||
};
|
||||
|
||||
private get threadRelation(): IEventRelation {
|
||||
const lastThreadReply = this.state.thread?.lastReply((ev: MatrixEvent) => {
|
||||
return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status;
|
||||
});
|
||||
|
||||
return {
|
||||
const relation = {
|
||||
"rel_type": THREAD_RELATION_TYPE.name,
|
||||
"event_id": this.state.thread?.id,
|
||||
"is_falling_back": true,
|
||||
"m.in_reply_to": {
|
||||
"event_id": lastThreadReply?.getId() ?? this.state.thread?.id,
|
||||
},
|
||||
};
|
||||
|
||||
const fallbackEventId = this.state.lastReply?.getId() ?? this.state.thread?.id;
|
||||
if (fallbackEventId) {
|
||||
relation["m.in_reply_to"] = {
|
||||
"event_id": fallbackEventId,
|
||||
};
|
||||
}
|
||||
|
||||
return relation;
|
||||
}
|
||||
|
||||
private renderThreadViewHeader = (): JSX.Element => {
|
||||
|
@ -314,7 +365,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
|
||||
const threadRelation = this.threadRelation;
|
||||
|
||||
let timeline: JSX.Element;
|
||||
let timeline: JSX.Element | null;
|
||||
if (this.state.thread) {
|
||||
if (this.props.initialEvent && this.props.initialEvent.getRoomId() !== this.state.thread.roomId) {
|
||||
logger.warn("ThreadView attempting to render TimelinePanel with mismatched initialEvent",
|
||||
|
|
396
src/components/views/auth/LoginWithQR.tsx
Normal file
396
src/components/views/auth/LoginWithQR.tsx
Normal file
|
@ -0,0 +1,396 @@
|
|||
/*
|
||||
Copyright 2022 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 { MSC3906Rendezvous, MSC3906RendezvousPayload, RendezvousFailureReason } from 'matrix-js-sdk/src/rendezvous';
|
||||
import { MSC3886SimpleHttpRendezvousTransport } from 'matrix-js-sdk/src/rendezvous/transports';
|
||||
import { MSC3903ECDHPayload, MSC3903ECDHv1RendezvousChannel } from 'matrix-js-sdk/src/rendezvous/channels';
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import QRCode from '../elements/QRCode';
|
||||
import Spinner from '../elements/Spinner';
|
||||
import { Icon as BackButtonIcon } from "../../../../res/img/element-icons/back.svg";
|
||||
import { Icon as DevicesIcon } from "../../../../res/img/element-icons/devices.svg";
|
||||
import { Icon as WarningBadge } from "../../../../res/img/element-icons/warning-badge.svg";
|
||||
import { Icon as InfoIcon } from "../../../../res/img/element-icons/i.svg";
|
||||
import { wrapRequestWithDialog } from '../../../utils/UserInteractiveAuth';
|
||||
|
||||
/**
|
||||
* The intention of this enum is to have a mode that scans a QR code instead of generating one.
|
||||
*/
|
||||
export enum Mode {
|
||||
/**
|
||||
* A QR code with be generated and shown
|
||||
*/
|
||||
Show = "show",
|
||||
}
|
||||
|
||||
enum Phase {
|
||||
Loading,
|
||||
ShowingQR,
|
||||
Connecting,
|
||||
Connected,
|
||||
WaitingForDevice,
|
||||
Verifying,
|
||||
Error,
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
client: MatrixClient;
|
||||
mode: Mode;
|
||||
onFinished(...args: any): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
phase: Phase;
|
||||
rendezvous?: MSC3906Rendezvous;
|
||||
confirmationDigits?: string;
|
||||
failureReason?: RendezvousFailureReason;
|
||||
mediaPermissionError?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that allows sign in and E2EE set up with a QR code.
|
||||
*
|
||||
* It implements both `login.start` and `login-reciprocate` capabilities as well as both scanning and showing QR codes.
|
||||
*
|
||||
* This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906
|
||||
*/
|
||||
export default class LoginWithQR extends React.Component<IProps, IState> {
|
||||
public constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
phase: Phase.Loading,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.updateMode(this.props.mode).then(() => {});
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>): void {
|
||||
if (prevProps.mode !== this.props.mode) {
|
||||
this.updateMode(this.props.mode).then(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
private async updateMode(mode: Mode) {
|
||||
this.setState({ phase: Phase.Loading });
|
||||
if (this.state.rendezvous) {
|
||||
this.state.rendezvous.onFailure = undefined;
|
||||
await this.state.rendezvous.cancel(RendezvousFailureReason.UserCancelled);
|
||||
this.setState({ rendezvous: undefined });
|
||||
}
|
||||
if (mode === Mode.Show) {
|
||||
await this.generateCode();
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
if (this.state.rendezvous) {
|
||||
// eslint-disable-next-line react/no-direct-mutation-state
|
||||
this.state.rendezvous.onFailure = undefined;
|
||||
// calling cancel will call close() as well to clean up the resources
|
||||
this.state.rendezvous.cancel(RendezvousFailureReason.UserCancelled).then(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
private approveLogin = async (): Promise<void> => {
|
||||
if (!this.state.rendezvous) {
|
||||
throw new Error('Rendezvous not found');
|
||||
}
|
||||
this.setState({ phase: Phase.Loading });
|
||||
|
||||
try {
|
||||
logger.info("Requesting login token");
|
||||
|
||||
const { login_token: loginToken } = await wrapRequestWithDialog(this.props.client.requestLoginToken, {
|
||||
matrixClient: this.props.client,
|
||||
title: _t("Sign in new device"),
|
||||
})();
|
||||
|
||||
this.setState({ phase: Phase.WaitingForDevice });
|
||||
|
||||
const newDeviceId = await this.state.rendezvous.approveLoginOnExistingDevice(loginToken);
|
||||
if (!newDeviceId) {
|
||||
// user denied
|
||||
return;
|
||||
}
|
||||
if (!this.props.client.crypto) {
|
||||
// no E2EE to set up
|
||||
this.props.onFinished(true);
|
||||
return;
|
||||
}
|
||||
await this.state.rendezvous.verifyNewDeviceOnExistingDevice();
|
||||
this.props.onFinished(true);
|
||||
} catch (e) {
|
||||
logger.error('Error whilst approving sign in', e);
|
||||
this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.Unknown });
|
||||
}
|
||||
};
|
||||
|
||||
private generateCode = async () => {
|
||||
let rendezvous: MSC3906Rendezvous;
|
||||
try {
|
||||
const transport = new MSC3886SimpleHttpRendezvousTransport<MSC3903ECDHPayload>({
|
||||
onFailure: this.onFailure,
|
||||
client: this.props.client,
|
||||
});
|
||||
|
||||
const channel = new MSC3903ECDHv1RendezvousChannel<MSC3906RendezvousPayload>(
|
||||
transport, undefined, this.onFailure,
|
||||
);
|
||||
|
||||
rendezvous = new MSC3906Rendezvous(channel, this.props.client, this.onFailure);
|
||||
|
||||
await rendezvous.generateCode();
|
||||
this.setState({
|
||||
phase: Phase.ShowingQR,
|
||||
rendezvous,
|
||||
failureReason: undefined,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('Error whilst generating QR code', e);
|
||||
this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.HomeserverLacksSupport });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const confirmationDigits = await rendezvous.startAfterShowingCode();
|
||||
this.setState({ phase: Phase.Connected, confirmationDigits });
|
||||
} catch (e) {
|
||||
logger.error('Error whilst doing QR login', e);
|
||||
// only set to error phase if it hasn't already been set by onFailure or similar
|
||||
if (this.state.phase !== Phase.Error) {
|
||||
this.setState({ phase: Phase.Error, failureReason: RendezvousFailureReason.Unknown });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private onFailure = (reason: RendezvousFailureReason) => {
|
||||
logger.info(`Rendezvous failed: ${reason}`);
|
||||
this.setState({ phase: Phase.Error, failureReason: reason });
|
||||
};
|
||||
|
||||
public reset() {
|
||||
this.setState({
|
||||
rendezvous: undefined,
|
||||
confirmationDigits: undefined,
|
||||
failureReason: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private cancelClicked = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled);
|
||||
this.reset();
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
private declineClicked = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
await this.state.rendezvous?.declineLoginOnExistingDevice();
|
||||
this.reset();
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
private tryAgainClicked = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
this.reset();
|
||||
await this.updateMode(this.props.mode);
|
||||
};
|
||||
|
||||
private onBackClick = async () => {
|
||||
await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled);
|
||||
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
private cancelButton = () => <AccessibleButton
|
||||
kind="primary_outline"
|
||||
onClick={this.cancelClicked}
|
||||
>
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>;
|
||||
|
||||
private simpleSpinner = (description?: string): JSX.Element => {
|
||||
return <div className="mx_LoginWithQR_spinner">
|
||||
<div>
|
||||
<Spinner />
|
||||
{ description && <p>{ description }</p> }
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
public render() {
|
||||
let title: string;
|
||||
let titleIcon: JSX.Element | undefined;
|
||||
let main: JSX.Element | undefined;
|
||||
let buttons: JSX.Element | undefined;
|
||||
let backButton = true;
|
||||
let cancellationMessage: string | undefined;
|
||||
let centreTitle = false;
|
||||
|
||||
switch (this.state.phase) {
|
||||
case Phase.Error:
|
||||
switch (this.state.failureReason) {
|
||||
case RendezvousFailureReason.Expired:
|
||||
cancellationMessage = _t("The linking wasn't completed in the required time.");
|
||||
break;
|
||||
case RendezvousFailureReason.InvalidCode:
|
||||
cancellationMessage = _t("The scanned code is invalid.");
|
||||
break;
|
||||
case RendezvousFailureReason.UnsupportedAlgorithm:
|
||||
cancellationMessage = _t("Linking with this device is not supported.");
|
||||
break;
|
||||
case RendezvousFailureReason.UserDeclined:
|
||||
cancellationMessage = _t("The request was declined on the other device.");
|
||||
break;
|
||||
case RendezvousFailureReason.OtherDeviceAlreadySignedIn:
|
||||
cancellationMessage = _t("The other device is already signed in.");
|
||||
break;
|
||||
case RendezvousFailureReason.OtherDeviceNotSignedIn:
|
||||
cancellationMessage = _t("The other device isn't signed in.");
|
||||
break;
|
||||
case RendezvousFailureReason.UserCancelled:
|
||||
cancellationMessage = _t("The request was cancelled.");
|
||||
break;
|
||||
case RendezvousFailureReason.Unknown:
|
||||
cancellationMessage = _t("An unexpected error occurred.");
|
||||
break;
|
||||
case RendezvousFailureReason.HomeserverLacksSupport:
|
||||
cancellationMessage = _t("The homeserver doesn't support signing in another device.");
|
||||
break;
|
||||
default:
|
||||
cancellationMessage = _t("The request was cancelled.");
|
||||
break;
|
||||
}
|
||||
title = _t("Connection failed");
|
||||
centreTitle = true;
|
||||
titleIcon = <WarningBadge className="error" />;
|
||||
backButton = false;
|
||||
main = <p data-testid="cancellation-message">{ cancellationMessage }</p>;
|
||||
buttons = <>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={this.tryAgainClicked}
|
||||
>
|
||||
{ _t("Try again") }
|
||||
</AccessibleButton>
|
||||
{ this.cancelButton() }
|
||||
</>;
|
||||
break;
|
||||
case Phase.Connected:
|
||||
title = _t("Devices connected");
|
||||
titleIcon = <DevicesIcon className="normal" />;
|
||||
backButton = false;
|
||||
main = <>
|
||||
<p>{ _t("Check that the code below matches with your other device:") }</p>
|
||||
<div className="mx_LoginWithQR_confirmationDigits">
|
||||
{ this.state.confirmationDigits }
|
||||
</div>
|
||||
<div className="mx_LoginWithQR_confirmationAlert">
|
||||
<div>
|
||||
<InfoIcon />
|
||||
</div>
|
||||
<div>{ _t("By approving access for this device, it will have full access to your account.") }</div>
|
||||
</div>
|
||||
</>;
|
||||
|
||||
buttons = <>
|
||||
<AccessibleButton
|
||||
data-testid="decline-login-button"
|
||||
kind="primary_outline"
|
||||
onClick={this.declineClicked}
|
||||
>
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
data-testid="approve-login-button"
|
||||
kind="primary"
|
||||
onClick={this.approveLogin}
|
||||
>
|
||||
{ _t("Approve") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
break;
|
||||
case Phase.ShowingQR:
|
||||
title =_t("Sign in with QR code");
|
||||
if (this.state.rendezvous) {
|
||||
const code = <div className="mx_LoginWithQR_qrWrapper">
|
||||
<QRCode data={[{ data: Buffer.from(this.state.rendezvous.code), mode: 'byte' }]} className="mx_QRCode" />
|
||||
</div>;
|
||||
main = <>
|
||||
<p>{ _t("Scan the QR code below with your device that's signed out.") }</p>
|
||||
<ol>
|
||||
<li>{ _t("Start at the sign in screen") }</li>
|
||||
<li>{ _t("Select 'Scan QR code'") }</li>
|
||||
<li>{ _t("Review and approve the sign in") }</li>
|
||||
</ol>
|
||||
{ code }
|
||||
</>;
|
||||
} else {
|
||||
main = this.simpleSpinner();
|
||||
buttons = this.cancelButton();
|
||||
}
|
||||
break;
|
||||
case Phase.Loading:
|
||||
main = this.simpleSpinner();
|
||||
break;
|
||||
case Phase.Connecting:
|
||||
main = this.simpleSpinner(_t("Connecting..."));
|
||||
buttons = this.cancelButton();
|
||||
break;
|
||||
case Phase.WaitingForDevice:
|
||||
main = this.simpleSpinner(_t("Waiting for device to sign in"));
|
||||
buttons = this.cancelButton();
|
||||
break;
|
||||
case Phase.Verifying:
|
||||
title = _t("Success");
|
||||
centreTitle = true;
|
||||
main = this.simpleSpinner(_t("Completing set up of your new device"));
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-testid="login-with-qr" className="mx_LoginWithQR">
|
||||
<div className={centreTitle ? "mx_LoginWithQR_centreTitle" : ""}>
|
||||
{ backButton ?
|
||||
<AccessibleButton
|
||||
data-testid="back-button"
|
||||
className="mx_LoginWithQR_BackButton"
|
||||
onClick={this.onBackClick}
|
||||
title="Back"
|
||||
>
|
||||
<BackButtonIcon />
|
||||
</AccessibleButton>
|
||||
: null }
|
||||
<h1>{ titleIcon }{ title }</h1>
|
||||
</div>
|
||||
<div className="mx_LoginWithQR_main">
|
||||
{ main }
|
||||
</div>
|
||||
<div className="mx_LoginWithQR_buttons">
|
||||
{ buttons }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
131
src/components/views/beacon/RoomCallBanner.tsx
Normal file
131
src/components/views/beacon/RoomCallBanner.tsx
Normal file
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
Copyright 2022 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, { useCallback } from "react";
|
||||
import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import dispatcher, { defaultDispatcher } from "../../../dispatcher/dispatcher";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { Call, ConnectionState, ElementCall } from "../../../models/Call";
|
||||
import { useCall } from "../../../hooks/useCall";
|
||||
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
import {
|
||||
OwnBeaconStore,
|
||||
OwnBeaconStoreEvent,
|
||||
} from "../../../stores/OwnBeaconStore";
|
||||
import { CallDurationFromEvent } from "../voip/CallDuration";
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
|
||||
interface RoomCallBannerProps {
|
||||
roomId: Room["roomId"];
|
||||
call: Call;
|
||||
}
|
||||
|
||||
const RoomCallBannerInner: React.FC<RoomCallBannerProps> = ({
|
||||
roomId,
|
||||
call,
|
||||
}) => {
|
||||
const callEvent: MatrixEvent | null = (call as ElementCall)?.groupCall;
|
||||
|
||||
const connect = useCallback(
|
||||
(ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: roomId,
|
||||
view_call: true,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
},
|
||||
[roomId],
|
||||
);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: roomId,
|
||||
metricsTrigger: undefined,
|
||||
event_id: callEvent.getId(),
|
||||
scroll_into_view: true,
|
||||
highlighted: true,
|
||||
});
|
||||
}, [callEvent, roomId]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="mx_RoomCallBanner"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="mx_RoomCallBanner_text">
|
||||
<span className="mx_RoomCallBanner_label">{ _t("Video call") }</span>
|
||||
<CallDurationFromEvent mxEvent={callEvent} />
|
||||
</div>
|
||||
|
||||
<AccessibleButton
|
||||
onClick={connect}
|
||||
kind="primary"
|
||||
element="button"
|
||||
disabled={false}
|
||||
>
|
||||
{ _t("Join") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
roomId: Room["roomId"];
|
||||
}
|
||||
|
||||
const RoomCallBanner: React.FC<Props> = ({ roomId }) => {
|
||||
const call = useCall(roomId);
|
||||
|
||||
// this section is to check if we have a live location share. If so, we dont show the call banner
|
||||
const isMonitoringLiveLocation = useEventEmitterState(
|
||||
OwnBeaconStore.instance,
|
||||
OwnBeaconStoreEvent.MonitoringLivePosition,
|
||||
() => OwnBeaconStore.instance.isMonitoringLiveLocation,
|
||||
);
|
||||
|
||||
const liveBeaconIds = useEventEmitterState(
|
||||
OwnBeaconStore.instance,
|
||||
OwnBeaconStoreEvent.LivenessChange,
|
||||
() => OwnBeaconStore.instance.getLiveBeaconIds(roomId),
|
||||
);
|
||||
|
||||
if (isMonitoringLiveLocation && liveBeaconIds.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if the call is already showing. No banner is needed in this case.
|
||||
if (SdkContextClass.instance.roomViewStore.isViewingCall()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Split into outer/inner to avoid watching various parts if there is no call
|
||||
if (call) {
|
||||
// No banner if the call is connected (or connecting/disconnecting)
|
||||
if (call.connectionState !== ConnectionState.Disconnected) return null;
|
||||
|
||||
return <RoomCallBannerInner call={call} roomId={roomId} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default RoomCallBanner;
|
|
@ -141,6 +141,7 @@ const RoomLiveShareWarning: React.FC<Props> = ({ roomId }) => {
|
|||
);
|
||||
|
||||
if (!isMonitoringLiveLocation || !liveBeaconIds.length) {
|
||||
// This logic is entangled with the RoomCallBanner-test's. The tests need updating if this logic changes.
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,6 @@ import Modal from "../../../Modal";
|
|||
import ExportDialog from "../dialogs/ExportDialog";
|
||||
import { useFeatureEnabled } from "../../../hooks/useSettings";
|
||||
import { usePinnedEvents } from "../right_panel/PinnedMessagesCard";
|
||||
import { RoomViewStore } from "../../../stores/RoomViewStore";
|
||||
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
|
||||
import { ROOM_NOTIFICATIONS_TAB } from "../dialogs/RoomSettingsDialog";
|
||||
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
|
@ -50,6 +49,7 @@ import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
|||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import DevtoolsDialog from "../dialogs/DevtoolsDialog";
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
|
||||
interface IProps extends IContextMenuProps {
|
||||
room: Room;
|
||||
|
@ -332,7 +332,7 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
|
|||
};
|
||||
|
||||
const ensureViewingRoom = (ev: ButtonEvent) => {
|
||||
if (RoomViewStore.instance.getRoomId() === room.roomId) return;
|
||||
if (SdkContextClass.instance.roomViewStore.getRoomId() === room.roomId) return;
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
|
@ -377,7 +377,7 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
|
|||
ev.stopPropagation();
|
||||
|
||||
Modal.createDialog(DevtoolsDialog, {
|
||||
roomId: RoomViewStore.instance.getRoomId(),
|
||||
roomId: SdkContextClass.instance.roomViewStore.getRoomId(),
|
||||
}, "mx_DevtoolsDialog_wrapper");
|
||||
onFinished();
|
||||
}}
|
||||
|
|
|
@ -29,9 +29,9 @@ import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
|||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
|
||||
interface IProps {
|
||||
export interface ThreadListContextMenuProps {
|
||||
mxEvent: MatrixEvent;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
onMenuToggle?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ const contextMenuBelow = (elementRect: DOMRect) => {
|
|||
return { left, top, chevronFace };
|
||||
};
|
||||
|
||||
const ThreadListContextMenu: React.FC<IProps> = ({
|
||||
const ThreadListContextMenu: React.FC<ThreadListContextMenuProps> = ({
|
||||
mxEvent,
|
||||
permalinkCreator,
|
||||
onMenuToggle,
|
||||
|
@ -64,12 +64,14 @@ const ThreadListContextMenu: React.FC<IProps> = ({
|
|||
closeThreadOptions();
|
||||
}, [mxEvent, closeThreadOptions]);
|
||||
|
||||
const copyLinkToThread = useCallback(async (evt: ButtonEvent) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const matrixToUrl = permalinkCreator.forEvent(mxEvent.getId());
|
||||
await copyPlaintext(matrixToUrl);
|
||||
closeThreadOptions();
|
||||
const copyLinkToThread = useCallback(async (evt: ButtonEvent | undefined) => {
|
||||
if (permalinkCreator) {
|
||||
evt?.preventDefault();
|
||||
evt?.stopPropagation();
|
||||
const matrixToUrl = permalinkCreator.forEvent(mxEvent.getId());
|
||||
await copyPlaintext(matrixToUrl);
|
||||
closeThreadOptions();
|
||||
}
|
||||
}, [mxEvent, closeThreadOptions, permalinkCreator]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -87,6 +89,7 @@ const ThreadListContextMenu: React.FC<IProps> = ({
|
|||
title={_t("Thread options")}
|
||||
isExpanded={menuDisplayed}
|
||||
inputRef={button}
|
||||
data-testid="threadlist-dropdown-button"
|
||||
/>
|
||||
{ menuDisplayed && (<IconizedContextMenu
|
||||
onFinished={closeThreadOptions}
|
||||
|
@ -102,11 +105,14 @@ const ThreadListContextMenu: React.FC<IProps> = ({
|
|||
label={_t("View in room")}
|
||||
iconClassName="mx_ThreadPanel_viewInRoom"
|
||||
/> }
|
||||
<IconizedContextMenuOption
|
||||
onClick={(e) => copyLinkToThread(e)}
|
||||
label={_t("Copy link to thread")}
|
||||
iconClassName="mx_ThreadPanel_copyLinkToThread"
|
||||
/>
|
||||
{ permalinkCreator &&
|
||||
<IconizedContextMenuOption
|
||||
data-testid="copy-thread-link"
|
||||
onClick={(e) => copyLinkToThread(e)}
|
||||
label={_t("Copy link to thread")}
|
||||
iconClassName="mx_ThreadPanel_copyLinkToThread"
|
||||
/>
|
||||
}
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>) }
|
||||
</React.Fragment>;
|
||||
|
|
|
@ -38,7 +38,7 @@ interface IDialogAesthetics {
|
|||
};
|
||||
}
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
export interface InteractiveAuthDialogProps extends IDialogProps {
|
||||
// matrix client to use for UI auth requests
|
||||
matrixClient: MatrixClient;
|
||||
|
||||
|
@ -82,8 +82,8 @@ interface IState {
|
|||
uiaStagePhase: number | string;
|
||||
}
|
||||
|
||||
export default class InteractiveAuthDialog extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
export default class InteractiveAuthDialog extends React.Component<InteractiveAuthDialogProps, IState> {
|
||||
constructor(props: InteractiveAuthDialogProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
|
|
|
@ -21,10 +21,11 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import { OIDCState, WidgetPermissionStore } from "../../../stores/widgets/WidgetPermissionStore";
|
||||
import { OIDCState } from "../../../stores/widgets/WidgetPermissionStore";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import { SdkContextClass } from '../../../contexts/SDKContext';
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
widget: Widget;
|
||||
|
@ -57,7 +58,7 @@ export default class WidgetOpenIDPermissionsDialog extends React.PureComponent<I
|
|||
if (this.state.rememberSelection) {
|
||||
logger.log(`Remembering ${this.props.widget.id} as allowed=${allowed} for OpenID`);
|
||||
|
||||
WidgetPermissionStore.instance.setOIDCState(
|
||||
SdkContextClass.instance.widgetPermissionStore.setOIDCState(
|
||||
this.props.widget, this.props.widgetKind, this.props.inRoomId,
|
||||
allowed ? OIDCState.Allowed : OIDCState.Denied,
|
||||
);
|
||||
|
|
|
@ -66,7 +66,7 @@ import { BreadcrumbsStore } from "../../../../stores/BreadcrumbsStore";
|
|||
import { RoomNotificationState } from "../../../../stores/notifications/RoomNotificationState";
|
||||
import { RoomNotificationStateStore } from "../../../../stores/notifications/RoomNotificationStateStore";
|
||||
import { RecentAlgorithm } from "../../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
|
||||
import { RoomViewStore } from "../../../../stores/RoomViewStore";
|
||||
import { SdkContextClass } from "../../../../contexts/SDKContext";
|
||||
import { getMetaSpaceName } from "../../../../stores/spaces";
|
||||
import SpaceStore from "../../../../stores/spaces/SpaceStore";
|
||||
import { DirectoryMember, Member, startDmOnFirstMessage } from "../../../../utils/direct-messages";
|
||||
|
@ -1060,7 +1060,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
</h4>
|
||||
<div>
|
||||
{ BreadcrumbsStore.instance.rooms
|
||||
.filter(r => r.roomId !== RoomViewStore.instance.getRoomId())
|
||||
.filter(r => r.roomId !== SdkContextClass.instance.roomViewStore.getRoomId())
|
||||
.map(room => (
|
||||
<TooltipOption
|
||||
id={`mx_SpotlightDialog_button_recentlyViewed_${room.roomId}`}
|
||||
|
|
|
@ -43,13 +43,13 @@ import { IApp } from "../../../stores/WidgetStore";
|
|||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { OwnProfileStore } from '../../../stores/OwnProfileStore';
|
||||
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
|
||||
import { RoomViewStore } from '../../../stores/RoomViewStore';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
import { ElementWidgetCapabilities } from '../../../stores/widgets/ElementWidgetCapabilities';
|
||||
import { WidgetMessagingStore } from '../../../stores/widgets/WidgetMessagingStore';
|
||||
import { SdkContextClass } from '../../../contexts/SDKContext';
|
||||
|
||||
interface IProps {
|
||||
app: IApp;
|
||||
|
@ -175,7 +175,7 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
);
|
||||
if (isActiveWidget) {
|
||||
// We just left the room that the active widget was from.
|
||||
if (this.props.room && RoomViewStore.instance.getRoomId() !== this.props.room.roomId) {
|
||||
if (this.props.room && SdkContextClass.instance.roomViewStore.getRoomId() !== this.props.room.roomId) {
|
||||
// If we are not actively looking at the room then destroy this widget entirely.
|
||||
this.endWidgetActions();
|
||||
} else if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||
|
|
|
@ -40,6 +40,7 @@ export default class Spinner extends React.PureComponent<IProps> {
|
|||
style={{ width: w, height: h }}
|
||||
aria-label={_t("Loading...")}
|
||||
role="progressbar"
|
||||
data-testid="spinner"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -57,7 +57,7 @@ type State = Partial<Pick<CSSProperties, "display" | "right" | "top" | "transfor
|
|||
|
||||
export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
|
||||
private static container: HTMLElement;
|
||||
private parent: Element;
|
||||
private parent: Element | null = null;
|
||||
|
||||
// XXX: This is because some components (Field) are unable to `import` the Tooltip class,
|
||||
// so we expose the Alignment options off of us statically.
|
||||
|
@ -87,7 +87,7 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
|
|||
capture: true,
|
||||
});
|
||||
|
||||
this.parent = ReactDOM.findDOMNode(this).parentNode as Element;
|
||||
this.parent = ReactDOM.findDOMNode(this)?.parentNode as Element ?? null;
|
||||
|
||||
this.updatePosition();
|
||||
}
|
||||
|
@ -109,7 +109,7 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
|
|||
// positioned, also taking into account any window zoom
|
||||
private updatePosition = (): void => {
|
||||
// When the tooltip is hidden, no need to thrash the DOM with `style` attribute updates (performance)
|
||||
if (!this.props.visible) return;
|
||||
if (!this.props.visible || !this.parent) return;
|
||||
|
||||
const parentBox = this.parent.getBoundingClientRect();
|
||||
const width = UIStore.instance.windowWidth;
|
||||
|
|
|
@ -69,6 +69,7 @@ export const LocationButton: React.FC<IProps> = ({ roomId, sender, menuPosition,
|
|||
iconClassName="mx_MessageComposer_location"
|
||||
onClick={openMenu}
|
||||
title={_t("Location")}
|
||||
inputRef={button}
|
||||
/>
|
||||
|
||||
{ contextMenu }
|
||||
|
|
|
@ -83,7 +83,7 @@ const OptionsButton: React.FC<IOptionsButtonProps> = ({
|
|||
getRelationsForEvent,
|
||||
}) => {
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex(button);
|
||||
const [onFocus, isActive] = useRovingTabIndex(button);
|
||||
useEffect(() => {
|
||||
onFocusChange(menuDisplayed);
|
||||
}, [onFocusChange, menuDisplayed]);
|
||||
|
@ -123,7 +123,7 @@ const OptionsButton: React.FC<IOptionsButtonProps> = ({
|
|||
onClick={onOptionsClick}
|
||||
onContextMenu={onOptionsClick}
|
||||
isExpanded={menuDisplayed}
|
||||
inputRef={ref}
|
||||
inputRef={button}
|
||||
onFocus={onFocus}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
>
|
||||
|
@ -141,7 +141,7 @@ interface IReactButtonProps {
|
|||
|
||||
const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusChange }) => {
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex(button);
|
||||
const [onFocus, isActive] = useRovingTabIndex(button);
|
||||
useEffect(() => {
|
||||
onFocusChange(menuDisplayed);
|
||||
}, [onFocusChange, menuDisplayed]);
|
||||
|
@ -173,7 +173,7 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC
|
|||
onClick={onClick}
|
||||
onContextMenu={onClick}
|
||||
isExpanded={menuDisplayed}
|
||||
inputRef={ref}
|
||||
inputRef={button}
|
||||
onFocus={onFocus}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
>
|
||||
|
|
|
@ -43,8 +43,6 @@ import MjolnirBody from "./MjolnirBody";
|
|||
import MBeaconBody from "./MBeaconBody";
|
||||
import { IEventTileOps } from "../rooms/EventTile";
|
||||
import { VoiceBroadcastBody, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from '../../../voice-broadcast';
|
||||
import { Features } from '../../../settings/Settings';
|
||||
import { SettingLevel } from '../../../settings/SettingLevel';
|
||||
|
||||
// onMessageAllowed is handled internally
|
||||
interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper"> {
|
||||
|
@ -58,18 +56,10 @@ interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper
|
|||
isSeeingThroughMessageHiddenForModeration?: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
voiceBroadcastEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface IOperableEventTile {
|
||||
getEventTileOps(): IEventTileOps;
|
||||
}
|
||||
|
||||
interface State {
|
||||
voiceBroadcastEnabled: boolean;
|
||||
}
|
||||
|
||||
const baseBodyTypes = new Map<string, typeof React.Component>([
|
||||
[MsgType.Text, TextualBody],
|
||||
[MsgType.Notice, TextualBody],
|
||||
|
@ -87,7 +77,7 @@ const baseEvTypes = new Map<string, React.ComponentType<Partial<IBodyProps>>>([
|
|||
[M_BEACON_INFO.altName, MBeaconBody],
|
||||
]);
|
||||
|
||||
export default class MessageEvent extends React.Component<IProps, State> implements IMediaBody, IOperableEventTile {
|
||||
export default class MessageEvent extends React.Component<IProps> implements IMediaBody, IOperableEventTile {
|
||||
private body: React.RefObject<React.Component | IOperableEventTile> = createRef();
|
||||
private mediaHelper: MediaEventHelper;
|
||||
private bodyTypes = new Map<string, typeof React.Component>(baseBodyTypes.entries());
|
||||
|
@ -95,7 +85,6 @@ export default class MessageEvent extends React.Component<IProps, State> impleme
|
|||
|
||||
public static contextType = MatrixClientContext;
|
||||
public context!: React.ContextType<typeof MatrixClientContext>;
|
||||
private voiceBroadcastSettingWatcherRef: string;
|
||||
|
||||
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
||||
super(props, context);
|
||||
|
@ -105,29 +94,15 @@ export default class MessageEvent extends React.Component<IProps, State> impleme
|
|||
}
|
||||
|
||||
this.updateComponentMaps();
|
||||
|
||||
this.state = {
|
||||
// only check voice broadcast settings for a voice broadcast event
|
||||
voiceBroadcastEnabled: this.props.mxEvent.getType() === VoiceBroadcastInfoEventType
|
||||
&& SettingsStore.getValue(Features.VoiceBroadcast),
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.props.mxEvent.addListener(MatrixEventEvent.Decrypted, this.onDecrypted);
|
||||
|
||||
if (this.props.mxEvent.getType() === VoiceBroadcastInfoEventType) {
|
||||
this.watchVoiceBroadcastFeatureSetting();
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.props.mxEvent.removeListener(MatrixEventEvent.Decrypted, this.onDecrypted);
|
||||
this.mediaHelper?.destroy();
|
||||
|
||||
if (this.voiceBroadcastSettingWatcherRef) {
|
||||
SettingsStore.unwatchSetting(this.voiceBroadcastSettingWatcherRef);
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>) {
|
||||
|
@ -171,16 +146,6 @@ export default class MessageEvent extends React.Component<IProps, State> impleme
|
|||
this.forceUpdate();
|
||||
};
|
||||
|
||||
private watchVoiceBroadcastFeatureSetting(): void {
|
||||
this.voiceBroadcastSettingWatcherRef = SettingsStore.watchSetting(
|
||||
Features.VoiceBroadcast,
|
||||
null,
|
||||
(settingName: string, roomId: string, atLevel: SettingLevel, newValAtLevel, newValue: boolean) => {
|
||||
this.setState({ voiceBroadcastEnabled: newValue });
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const type = this.props.mxEvent.getType();
|
||||
|
@ -209,8 +174,7 @@ export default class MessageEvent extends React.Component<IProps, State> impleme
|
|||
}
|
||||
|
||||
if (
|
||||
this.state.voiceBroadcastEnabled
|
||||
&& type === VoiceBroadcastInfoEventType
|
||||
type === VoiceBroadcastInfoEventType
|
||||
&& content?.state === VoiceBroadcastInfoState.Started
|
||||
) {
|
||||
BodyType = VoiceBroadcastBody;
|
||||
|
|
|
@ -33,7 +33,6 @@ import dis from '../../../dispatcher/dispatcher';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
import { RoomViewStore } from '../../../stores/RoomViewStore';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
import UploadBar from '../../structures/UploadBar';
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
|
@ -42,6 +41,7 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
|||
import Measured from '../elements/Measured';
|
||||
import Heading from '../typography/Heading';
|
||||
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
|
||||
import { SdkContextClass } from '../../../contexts/SDKContext';
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -91,7 +91,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
RoomViewStore.instance.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
SdkContextClass.instance.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.readReceiptsSettingWatcher = SettingsStore.watchSetting("showReadReceipts", null, (...[,,, value]) =>
|
||||
this.setState({ showReadReceipts: value as boolean }),
|
||||
|
@ -102,7 +102,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
RoomViewStore.instance.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
SdkContextClass.instance.roomViewStore.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
|
||||
if (this.readReceiptsSettingWatcher) {
|
||||
SettingsStore.unwatchSetting(this.readReceiptsSettingWatcher);
|
||||
|
@ -116,12 +116,9 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
|||
|
||||
private onRoomViewStoreUpdate = async (initial?: boolean): Promise<void> => {
|
||||
const newState: Pick<IState, any> = {
|
||||
// roomLoading: RoomViewStore.instance.isRoomLoading(),
|
||||
// roomLoadError: RoomViewStore.instance.getRoomLoadError(),
|
||||
|
||||
initialEventId: RoomViewStore.instance.getInitialEventId(),
|
||||
isInitialEventHighlighted: RoomViewStore.instance.isInitialEventHighlighted(),
|
||||
replyToEvent: RoomViewStore.instance.getQuotingEvent(),
|
||||
initialEventId: SdkContextClass.instance.roomViewStore.getInitialEventId(),
|
||||
isInitialEventHighlighted: SdkContextClass.instance.roomViewStore.isInitialEventHighlighted(),
|
||||
replyToEvent: SdkContextClass.instance.roomViewStore.getQuotingEvent(),
|
||||
};
|
||||
|
||||
this.setState(newState);
|
||||
|
|
|
@ -36,7 +36,6 @@ import { _t } from '../../../languageHandler';
|
|||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import { RoomViewStore } from "../../../stores/RoomViewStore";
|
||||
import MultiInviter from "../../../utils/MultiInviter";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import E2EIcon from "../rooms/E2EIcon";
|
||||
|
@ -77,6 +76,7 @@ import UserIdentifierCustomisations from '../../../customisations/UserIdentifier
|
|||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { DirectoryMember, startDmOnFirstMessage } from '../../../utils/direct-messages';
|
||||
import { SdkContextClass } from '../../../contexts/SDKContext';
|
||||
|
||||
export interface IDevice {
|
||||
deviceId: string;
|
||||
|
@ -412,7 +412,7 @@ const UserOptionsSection: React.FC<{
|
|||
}
|
||||
|
||||
if (canInvite && (member?.membership ?? 'leave') === 'leave' && shouldShowComponent(UIComponent.InviteUsers)) {
|
||||
const roomId = member && member.roomId ? member.roomId : RoomViewStore.instance.getRoomId();
|
||||
const roomId = member && member.roomId ? member.roomId : SdkContextClass.instance.roomViewStore.getRoomId();
|
||||
const onInviteUserButton = async (ev: ButtonEvent) => {
|
||||
try {
|
||||
// We use a MultiInviter to re-use the invite logic, even though we're only inviting one user.
|
||||
|
|
|
@ -31,7 +31,6 @@ import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete';
|
|||
import { getAutoCompleteCreator, Part, Type } from '../../../editor/parts';
|
||||
import { parseEvent, parsePlainTextMessage } from '../../../editor/deserialize';
|
||||
import { renderModel } from '../../../editor/render';
|
||||
import TypingStore from "../../../stores/TypingStore";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { IS_MAC, Key } from "../../../Keyboard";
|
||||
import { EMOTICON_TO_EMOJI } from "../../../emoji";
|
||||
|
@ -47,6 +46,7 @@ import { getKeyBindingsManager } from '../../../KeyBindingsManager';
|
|||
import { ALTERNATE_KEY_NAME, KeyBindingAction } from '../../../accessibility/KeyboardShortcuts';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { linkify } from '../../../linkify-matrix';
|
||||
import { SdkContextClass } from '../../../contexts/SDKContext';
|
||||
|
||||
// matches emoticons which follow the start of a line or whitespace
|
||||
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s|:^$');
|
||||
|
@ -246,7 +246,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
isTyping = false;
|
||||
}
|
||||
}
|
||||
TypingStore.sharedInstance().setSelfTyping(
|
||||
SdkContextClass.instance.typingStore.setSelfTyping(
|
||||
this.props.room.roomId,
|
||||
this.props.threadId,
|
||||
isTyping,
|
||||
|
@ -789,6 +789,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
aria-activedescendant={activeDescendant}
|
||||
dir="auto"
|
||||
aria-disabled={this.props.disabled}
|
||||
data-testid="basicmessagecomposer"
|
||||
/>
|
||||
</div>);
|
||||
}
|
||||
|
|
|
@ -932,6 +932,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
rightClick={true}
|
||||
reactions={this.state.reactions}
|
||||
link={this.state.contextMenu.link}
|
||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -73,6 +73,7 @@ function SendButton(props: ISendButtonProps) {
|
|||
className="mx_MessageComposer_sendMessage"
|
||||
onClick={props.onClick}
|
||||
title={props.title ?? _t('Send message')}
|
||||
data-testid="sendmessagebtn"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -85,7 +86,6 @@ interface IProps {
|
|||
relation?: IEventRelation;
|
||||
e2eStatus?: E2EStatus;
|
||||
compact?: boolean;
|
||||
showVoiceBroadcastButton?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -384,10 +384,6 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
return this.state.showStickersButton && !isLocalRoom(this.props.room);
|
||||
}
|
||||
|
||||
private get showVoiceBroadcastButton(): boolean {
|
||||
return this.props.showVoiceBroadcastButton && this.state.showVoiceBroadcastButton;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer");
|
||||
const controls = [
|
||||
|
@ -532,10 +528,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
showPollsButton={this.state.showPollsButton}
|
||||
showStickersButton={this.showStickersButton}
|
||||
toggleButtonMenu={this.toggleButtonMenu}
|
||||
showVoiceBroadcastButton={this.showVoiceBroadcastButton}
|
||||
showVoiceBroadcastButton={this.state.showVoiceBroadcastButton}
|
||||
onStartVoiceBroadcastClick={() => {
|
||||
startNewVoiceBroadcastRecording(
|
||||
this.props.room.roomId,
|
||||
this.props.room,
|
||||
MatrixClientPeg.get(),
|
||||
VoiceBroadcastRecordingsStore.instance(),
|
||||
);
|
||||
|
|
|
@ -179,6 +179,7 @@ const EmojiButton: React.FC<IEmojiButtonProps> = ({ addEmoji, menuPosition }) =>
|
|||
iconClassName="mx_MessageComposer_emoji"
|
||||
onClick={openMenu}
|
||||
title={_t("Emoji")}
|
||||
inputRef={button}
|
||||
/>
|
||||
|
||||
{ contextMenu }
|
||||
|
|
|
@ -209,7 +209,8 @@ export function ReadReceiptGroup(
|
|||
onMouseOver={showTooltip}
|
||||
onMouseLeave={hideTooltip}
|
||||
onFocus={showTooltip}
|
||||
onBlur={hideTooltip}>
|
||||
onBlur={hideTooltip}
|
||||
>
|
||||
{ remText }
|
||||
<span
|
||||
className="mx_ReadReceiptGroup_container"
|
||||
|
|
|
@ -67,6 +67,7 @@ import IconizedContextMenu, {
|
|||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { CallDurationFromEvent } from "../voip/CallDuration";
|
||||
import { Alignment } from "../elements/Tooltip";
|
||||
import RoomCallBanner from '../beacon/RoomCallBanner';
|
||||
|
||||
class DisabledWithReason {
|
||||
constructor(public readonly reason: string) { }
|
||||
|
@ -733,6 +734,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
{ betaPill }
|
||||
{ buttons }
|
||||
</div>
|
||||
{ !isVideoRoom && <RoomCallBanner roomId={this.props.room.roomId} /> }
|
||||
<RoomLiveShareWarning roomId={this.props.room.roomId} />
|
||||
</header>
|
||||
);
|
||||
|
|
|
@ -38,7 +38,6 @@ import { ITagMap } from "../../../stores/room-list/algorithms/models";
|
|||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
||||
import { RoomViewStore } from "../../../stores/RoomViewStore";
|
||||
import {
|
||||
isMetaSpace,
|
||||
ISuggestedRoom,
|
||||
|
@ -62,6 +61,7 @@ import IconizedContextMenu, {
|
|||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import ExtraTile from "./ExtraTile";
|
||||
import RoomSublist, { IAuxButtonProps } from "./RoomSublist";
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
|
||||
interface IProps {
|
||||
onKeyDown: (ev: React.KeyboardEvent, state: IRovingTabIndexState) => void;
|
||||
|
@ -421,7 +421,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
|
||||
public componentDidMount(): void {
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
RoomViewStore.instance.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
SdkContextClass.instance.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
SpaceStore.instance.on(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms);
|
||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
|
||||
this.favouriteMessageWatcher =
|
||||
|
@ -436,19 +436,19 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
|
||||
SettingsStore.unwatchSetting(this.favouriteMessageWatcher);
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
RoomViewStore.instance.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
SdkContextClass.instance.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
}
|
||||
|
||||
private onRoomViewStoreUpdate = () => {
|
||||
this.setState({
|
||||
currentRoomId: RoomViewStore.instance.getRoomId(),
|
||||
currentRoomId: SdkContextClass.instance.roomViewStore.getRoomId(),
|
||||
});
|
||||
};
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === Action.ViewRoomDelta) {
|
||||
const viewRoomDeltaPayload = payload as ViewRoomDeltaPayload;
|
||||
const currentRoomId = RoomViewStore.instance.getRoomId();
|
||||
const currentRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
|
||||
const room = this.getRoomDelta(currentRoomId, viewRoomDeltaPayload.delta, viewRoomDeltaPayload.unread);
|
||||
if (room) {
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
|
|
|
@ -379,7 +379,7 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
|||
isExpanded={mainMenuDisplayed}
|
||||
className="mx_RoomListHeader_contextMenuButton"
|
||||
title={activeSpace
|
||||
? _t("%(spaceName)s menu", { spaceName })
|
||||
? _t("%(spaceName)s menu", { spaceName: spaceName ?? activeSpace.name })
|
||||
: _t("Home options")}
|
||||
>
|
||||
{ title }
|
||||
|
|
|
@ -44,10 +44,10 @@ import PosthogTrackers from "../../../PosthogTrackers";
|
|||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
import { RoomViewStore } from "../../../stores/RoomViewStore";
|
||||
import { RoomTileCallSummary } from "./RoomTileCallSummary";
|
||||
import { RoomGeneralContextMenu } from "../context_menus/RoomGeneralContextMenu";
|
||||
import { CallStore, CallStoreEvent } from "../../../stores/CallStore";
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -86,7 +86,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
selected: RoomViewStore.instance.getRoomId() === this.props.room.roomId,
|
||||
selected: SdkContextClass.instance.roomViewStore.getRoomId() === this.props.room.roomId,
|
||||
notificationsMenuPosition: null,
|
||||
generalMenuPosition: null,
|
||||
call: CallStore.instance.getCall(this.props.room.roomId),
|
||||
|
@ -146,7 +146,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
this.scrollIntoView();
|
||||
}
|
||||
|
||||
RoomViewStore.instance.addRoomListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||
SdkContextClass.instance.roomViewStore.addRoomListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
MessagePreviewStore.instance.on(
|
||||
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
|
||||
|
@ -163,7 +163,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
RoomViewStore.instance.removeRoomListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||
SdkContextClass.instance.roomViewStore.removeRoomListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||
MessagePreviewStore.instance.off(
|
||||
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
|
||||
this.onRoomPreviewChanged,
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { useWysiwyg } from "@matrix-org/matrix-wysiwyg";
|
||||
import { useWysiwyg, Wysiwyg, WysiwygInputEvent } from "@matrix-org/matrix-wysiwyg";
|
||||
|
||||
import { Editor } from './Editor';
|
||||
import { FormattingButtons } from './FormattingButtons';
|
||||
|
@ -25,6 +25,7 @@ import { sendMessage } from './message';
|
|||
import { useMatrixClientContext } from '../../../../contexts/MatrixClientContext';
|
||||
import { useRoomContext } from '../../../../contexts/RoomContext';
|
||||
import { useWysiwygActionHandler } from './useWysiwygActionHandler';
|
||||
import { useSettingValue } from '../../../../hooks/useSettings';
|
||||
|
||||
interface WysiwygProps {
|
||||
disabled?: boolean;
|
||||
|
@ -41,8 +42,27 @@ export function WysiwygComposer(
|
|||
) {
|
||||
const roomContext = useRoomContext();
|
||||
const mxClient = useMatrixClientContext();
|
||||
const ctrlEnterToSend = useSettingValue("MessageComposerInput.ctrlEnterToSend");
|
||||
|
||||
const { ref, isWysiwygReady, content, formattingStates, wysiwyg } = useWysiwyg();
|
||||
function inputEventProcessor(event: WysiwygInputEvent, wysiwyg: Wysiwyg): WysiwygInputEvent | null {
|
||||
if (event instanceof ClipboardEvent) {
|
||||
return event;
|
||||
}
|
||||
|
||||
if (
|
||||
(event.inputType === 'insertParagraph' && !ctrlEnterToSend) ||
|
||||
event.inputType === 'sendMessage'
|
||||
) {
|
||||
sendMessage(content, { mxClient, roomContext, ...props });
|
||||
wysiwyg.actions.clear();
|
||||
ref.current?.focus();
|
||||
return null;
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
const { ref, isWysiwygReady, content, formattingStates, wysiwyg } = useWysiwyg({ inputEventProcessor });
|
||||
|
||||
useEffect(() => {
|
||||
if (!disabled && content !== null) {
|
||||
|
|
|
@ -19,13 +19,14 @@ import classNames from 'classnames';
|
|||
import { IMyDevice } from "matrix-js-sdk/src/client";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning";
|
||||
import { CryptoEvent } from 'matrix-js-sdk/src/crypto';
|
||||
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import DevicesPanelEntry from "./DevicesPanelEntry";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { deleteDevicesWithInteractiveAuth } from './devices/deleteDevices';
|
||||
import MatrixClientContext from '../../../contexts/MatrixClientContext';
|
||||
|
||||
interface IProps {
|
||||
className?: string;
|
||||
|
@ -40,6 +41,8 @@ interface IState {
|
|||
}
|
||||
|
||||
export default class DevicesPanel extends React.Component<IProps, IState> {
|
||||
public static contextType = MatrixClientContext;
|
||||
public context!: React.ContextType<typeof MatrixClientContext>;
|
||||
private unmounted = false;
|
||||
|
||||
constructor(props: IProps) {
|
||||
|
@ -52,15 +55,22 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.context.on(CryptoEvent.DevicesUpdated, this.onDevicesUpdated);
|
||||
this.loadDevices();
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.context.off(CryptoEvent.DevicesUpdated, this.onDevicesUpdated);
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
private onDevicesUpdated = (users: string[]) => {
|
||||
if (!users.includes(this.context.getUserId())) return;
|
||||
this.loadDevices();
|
||||
};
|
||||
|
||||
private loadDevices(): void {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const cli = this.context;
|
||||
cli.getDevices().then(
|
||||
(resp) => {
|
||||
if (this.unmounted) { return; }
|
||||
|
@ -111,7 +121,7 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
|
|||
|
||||
private isDeviceVerified(device: IMyDevice): boolean | null {
|
||||
try {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const cli = this.context;
|
||||
const deviceInfo = cli.getStoredDevice(cli.getUserId(), device.device_id);
|
||||
return this.state.crossSigningInfo.checkDeviceTrust(
|
||||
this.state.crossSigningInfo,
|
||||
|
@ -184,7 +194,7 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
|
|||
|
||||
try {
|
||||
await deleteDevicesWithInteractiveAuth(
|
||||
MatrixClientPeg.get(),
|
||||
this.context,
|
||||
this.state.selectedDevices,
|
||||
(success) => {
|
||||
if (success) {
|
||||
|
@ -208,7 +218,7 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private renderDevice = (device: IMyDevice): JSX.Element => {
|
||||
const myDeviceId = MatrixClientPeg.get().getDeviceId();
|
||||
const myDeviceId = this.context.getDeviceId();
|
||||
const myDevice = this.state.devices.find((device) => (device.device_id === myDeviceId));
|
||||
|
||||
const isOwnDevice = device.device_id === myDeviceId;
|
||||
|
@ -246,7 +256,7 @@ export default class DevicesPanel extends React.Component<IProps, IState> {
|
|||
return <Spinner />;
|
||||
}
|
||||
|
||||
const myDeviceId = MatrixClientPeg.get().getDeviceId();
|
||||
const myDeviceId = this.context.getDeviceId();
|
||||
const myDevice = devices.find((device) => (device.device_id === myDeviceId));
|
||||
|
||||
if (!myDevice) {
|
||||
|
|
|
@ -62,7 +62,6 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
id: 'session',
|
||||
values: [
|
||||
{ label: _t('Session ID'), value: device.device_id },
|
||||
{ label: _t('Client'), value: device.client },
|
||||
{
|
||||
label: _t('Last activity'),
|
||||
value: device.last_seen_ts && formatDate(new Date(device.last_seen_ts)),
|
||||
|
@ -84,6 +83,7 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
values: [
|
||||
{ label: _t('Model'), value: device.deviceModel },
|
||||
{ label: _t('Operating system'), value: device.deviceOperatingSystem },
|
||||
{ label: _t('Browser'), value: device.client },
|
||||
{ label: _t('IP address'), value: device.last_seen_ip },
|
||||
],
|
||||
},
|
||||
|
|
63
src/components/views/settings/devices/LoginWithQRSection.tsx
Normal file
63
src/components/views/settings/devices/LoginWithQRSection.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
Copyright 2022 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 type { IServerVersions } from 'matrix-js-sdk/src/matrix';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import AccessibleButton from '../../elements/AccessibleButton';
|
||||
import SettingsSubsection from '../shared/SettingsSubsection';
|
||||
import SettingsStore from '../../../../settings/SettingsStore';
|
||||
|
||||
interface IProps {
|
||||
onShowQr: () => void;
|
||||
versions: IServerVersions;
|
||||
}
|
||||
|
||||
export default class LoginWithQRSection extends React.Component<IProps> {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const msc3882Supported = !!this.props.versions?.unstable_features?.['org.matrix.msc3882'];
|
||||
const msc3886Supported = !!this.props.versions?.unstable_features?.['org.matrix.msc3886'];
|
||||
|
||||
// Needs to be enabled as a feature + server support MSC3886 or have a default rendezvous server configured:
|
||||
const offerShowQr = SettingsStore.getValue("feature_qr_signin_reciprocate_show") &&
|
||||
msc3882Supported && msc3886Supported; // We don't support configuration of a fallback at the moment so we just check the MSCs
|
||||
|
||||
// don't show anything if no method is available
|
||||
if (!offerShowQr) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <SettingsSubsection
|
||||
heading={_t('Sign in with QR code')}
|
||||
>
|
||||
<div className="mx_LoginWithQRSection">
|
||||
<p className="mx_SettingsTab_subsectionText">{
|
||||
_t("You can use this device to sign in a new device with a QR code. You will need to " +
|
||||
"scan the QR code shown on this device with your device that's signed out.")
|
||||
}</p>
|
||||
<AccessibleButton
|
||||
onClick={this.props.onShowQr}
|
||||
kind="primary"
|
||||
>{ _t("Show QR code") }</AccessibleButton>
|
||||
</div>
|
||||
</SettingsSubsection>;
|
||||
}
|
||||
}
|
|
@ -31,6 +31,7 @@ import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/reque
|
|||
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifications";
|
||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||
|
||||
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
|
@ -179,6 +180,12 @@ export const useOwnDevices = (): DevicesState => {
|
|||
refreshDevices();
|
||||
}, [refreshDevices]);
|
||||
|
||||
useEventEmitter(matrixClient, CryptoEvent.DevicesUpdated, (users: string[]): void => {
|
||||
if (users.includes(userId)) {
|
||||
refreshDevices();
|
||||
}
|
||||
});
|
||||
|
||||
useEventEmitter(matrixClient, ClientEvent.AccountData, (event: MatrixEvent): void => {
|
||||
const type = event.getType();
|
||||
if (type.startsWith(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) {
|
||||
|
|
|
@ -38,6 +38,9 @@ import InlineSpinner from "../../../elements/InlineSpinner";
|
|||
import { PosthogAnalytics } from "../../../../../PosthogAnalytics";
|
||||
import { showDialog as showAnalyticsLearnMoreDialog } from "../../../dialogs/AnalyticsLearnMoreDialog";
|
||||
import { privateShouldBeEncrypted } from "../../../../../utils/rooms";
|
||||
import LoginWithQR, { Mode } from '../../../auth/LoginWithQR';
|
||||
import LoginWithQRSection from '../../devices/LoginWithQRSection';
|
||||
import type { IServerVersions } from 'matrix-js-sdk/src/matrix';
|
||||
|
||||
interface IIgnoredUserProps {
|
||||
userId: string;
|
||||
|
@ -72,6 +75,8 @@ interface IState {
|
|||
waitingUnignored: string[];
|
||||
managingInvites: boolean;
|
||||
invitedRoomIds: Set<string>;
|
||||
showLoginWithQR: Mode | null;
|
||||
versions?: IServerVersions;
|
||||
}
|
||||
|
||||
export default class SecurityUserSettingsTab extends React.Component<IProps, IState> {
|
||||
|
@ -88,6 +93,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
|
|||
waitingUnignored: [],
|
||||
managingInvites: false,
|
||||
invitedRoomIds,
|
||||
showLoginWithQR: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -102,6 +108,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
|
|||
public componentDidMount(): void {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
MatrixClientPeg.get().on(RoomEvent.MyMembership, this.onMyMembership);
|
||||
MatrixClientPeg.get().getVersions().then(versions => this.setState({ versions }));
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
|
@ -251,6 +258,14 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
|
|||
);
|
||||
}
|
||||
|
||||
private onShowQRClicked = (): void => {
|
||||
this.setState({ showLoginWithQR: Mode.Show });
|
||||
};
|
||||
|
||||
private onLoginWithQRFinished = (): void => {
|
||||
this.setState({ showLoginWithQR: null });
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const secureBackup = (
|
||||
<div className='mx_SettingsTab_section'>
|
||||
|
@ -347,6 +362,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
|
|||
}
|
||||
|
||||
const useNewSessionManager = SettingsStore.getValue("feature_new_device_manager");
|
||||
const showQrCodeEnabled = SettingsStore.getValue("feature_qr_signin_reciprocate_show");
|
||||
const devicesSection = useNewSessionManager
|
||||
? null
|
||||
: <>
|
||||
|
@ -363,8 +379,20 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
|
|||
</span>
|
||||
<DevicesPanel />
|
||||
</div>
|
||||
{ showQrCodeEnabled ?
|
||||
<LoginWithQRSection onShowQr={this.onShowQRClicked} versions={this.state.versions} />
|
||||
: null
|
||||
}
|
||||
</>;
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
if (showQrCodeEnabled && this.state.showLoginWithQR) {
|
||||
return <div className="mx_SettingsTab mx_SecurityUserSettingsTab">
|
||||
<LoginWithQR onFinished={this.onLoginWithQRFinished} mode={this.state.showLoginWithQR} client={client} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
|
||||
{ warning }
|
||||
|
|
|
@ -32,6 +32,10 @@ import SecurityRecommendations from '../../devices/SecurityRecommendations';
|
|||
import { DeviceSecurityVariation, ExtendedDevice } from '../../devices/types';
|
||||
import { deleteDevicesWithInteractiveAuth } from '../../devices/deleteDevices';
|
||||
import SettingsTab from '../SettingsTab';
|
||||
import LoginWithQRSection from '../../devices/LoginWithQRSection';
|
||||
import LoginWithQR, { Mode } from '../../../auth/LoginWithQR';
|
||||
import SettingsStore from '../../../../../settings/SettingsStore';
|
||||
import { useAsyncMemo } from '../../../../../hooks/useAsyncMemo';
|
||||
|
||||
const useSignOut = (
|
||||
matrixClient: MatrixClient,
|
||||
|
@ -104,6 +108,7 @@ const SessionManagerTab: React.FC = () => {
|
|||
const matrixClient = useContext(MatrixClientContext);
|
||||
const userId = matrixClient.getUserId();
|
||||
const currentUserMember = userId && matrixClient.getUser(userId) || undefined;
|
||||
const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]);
|
||||
|
||||
const onDeviceExpandToggle = (deviceId: ExtendedDevice['device_id']): void => {
|
||||
if (expandedDeviceIds.includes(deviceId)) {
|
||||
|
@ -175,6 +180,26 @@ const SessionManagerTab: React.FC = () => {
|
|||
onSignOutOtherDevices(Object.keys(otherDevices));
|
||||
}: undefined;
|
||||
|
||||
const [signInWithQrMode, setSignInWithQrMode] = useState<Mode | null>();
|
||||
|
||||
const showQrCodeEnabled = SettingsStore.getValue("feature_qr_signin_reciprocate_show");
|
||||
|
||||
const onQrFinish = useCallback(() => {
|
||||
setSignInWithQrMode(null);
|
||||
}, [setSignInWithQrMode]);
|
||||
|
||||
const onShowQrClicked = useCallback(() => {
|
||||
setSignInWithQrMode(Mode.Show);
|
||||
}, [setSignInWithQrMode]);
|
||||
|
||||
if (showQrCodeEnabled && signInWithQrMode) {
|
||||
return <LoginWithQR
|
||||
mode={signInWithQrMode}
|
||||
onFinished={onQrFinish}
|
||||
client={matrixClient}
|
||||
/>;
|
||||
}
|
||||
|
||||
return <SettingsTab heading={_t('Sessions')}>
|
||||
<SecurityRecommendations
|
||||
devices={devices}
|
||||
|
@ -222,6 +247,10 @@ const SessionManagerTab: React.FC = () => {
|
|||
/>
|
||||
</SettingsSubsection>
|
||||
}
|
||||
{ showQrCodeEnabled ?
|
||||
<LoginWithQRSection onShowQr={onShowQrClicked} versions={clientVersions} />
|
||||
: null
|
||||
}
|
||||
</SettingsTab>;
|
||||
};
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ import { Icon as FavoriteIcon } from '../../../../res/img/element-icons/roomlist
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import Modal from "../../../Modal";
|
||||
import DevtoolsDialog from "../dialogs/DevtoolsDialog";
|
||||
import { RoomViewStore } from "../../../stores/RoomViewStore";
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
|
||||
const QuickSettingsButton = ({ isPanelCollapsed = false }) => {
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
|
||||
|
@ -72,7 +72,7 @@ const QuickSettingsButton = ({ isPanelCollapsed = false }) => {
|
|||
onClick={() => {
|
||||
closeMenu();
|
||||
Modal.createDialog(DevtoolsDialog, {
|
||||
roomId: RoomViewStore.instance.getRoomId(),
|
||||
roomId: SdkContextClass.instance.roomViewStore.getRoomId(),
|
||||
}, "mx_DevtoolsDialog_wrapper");
|
||||
}}
|
||||
kind="danger_outline"
|
||||
|
|
|
@ -204,9 +204,7 @@ const CreateSpaceButton = ({
|
|||
isPanelCollapsed,
|
||||
setPanelCollapsed,
|
||||
}: Pick<IInnerSpacePanelProps, "isPanelCollapsed" | "setPanelCollapsed">) => {
|
||||
// We don't need the handle as we position the menu in a constant location
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [menuDisplayed, _handle, openMenu, closeMenu] = useContextMenu<void>();
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLElement>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPanelCollapsed && menuDisplayed) {
|
||||
|
@ -231,13 +229,14 @@ const CreateSpaceButton = ({
|
|||
role="treeitem"
|
||||
>
|
||||
<SpaceButton
|
||||
data-test-id='create-space-button'
|
||||
data-testid='create-space-button'
|
||||
className={classNames("mx_SpaceButton_new", {
|
||||
mx_SpaceButton_newCancel: menuDisplayed,
|
||||
})}
|
||||
label={menuDisplayed ? _t("Cancel") : _t("Create a space")}
|
||||
onClick={onNewClick}
|
||||
isNarrow={isPanelCollapsed}
|
||||
ref={handle}
|
||||
/>
|
||||
|
||||
{ contextMenu }
|
||||
|
|
|
@ -14,7 +14,16 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { MouseEvent, ComponentProps, ComponentType, createRef, InputHTMLAttributes, LegacyRef } from "react";
|
||||
import React, {
|
||||
MouseEvent,
|
||||
ComponentProps,
|
||||
ComponentType,
|
||||
createRef,
|
||||
InputHTMLAttributes,
|
||||
LegacyRef,
|
||||
forwardRef,
|
||||
RefObject,
|
||||
} from "react";
|
||||
import classNames from "classnames";
|
||||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
|
||||
|
@ -54,7 +63,7 @@ interface IButtonProps extends Omit<ComponentProps<typeof AccessibleTooltipButto
|
|||
onClick?(ev?: ButtonEvent): void;
|
||||
}
|
||||
|
||||
export const SpaceButton: React.FC<IButtonProps> = ({
|
||||
export const SpaceButton = forwardRef<HTMLElement, IButtonProps>(({
|
||||
space,
|
||||
spaceKey,
|
||||
className,
|
||||
|
@ -67,9 +76,9 @@ export const SpaceButton: React.FC<IButtonProps> = ({
|
|||
children,
|
||||
ContextMenuComponent,
|
||||
...props
|
||||
}) => {
|
||||
const [menuDisplayed, ref, openMenu, closeMenu] = useContextMenu<HTMLElement>();
|
||||
const [onFocus, isActive, handle] = useRovingTabIndex(ref);
|
||||
}, ref: RefObject<HTMLElement>) => {
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLElement>(ref);
|
||||
const [onFocus, isActive] = useRovingTabIndex(handle);
|
||||
const tabIndex = isActive ? 0 : -1;
|
||||
|
||||
let avatar = <div className="mx_SpaceButton_avatarPlaceholder"><div className="mx_SpaceButton_icon" /></div>;
|
||||
|
@ -150,7 +159,7 @@ export const SpaceButton: React.FC<IButtonProps> = ({
|
|||
</div>
|
||||
</AccessibleTooltipButton>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
interface IItemProps extends InputHTMLAttributes<HTMLLIElement> {
|
||||
space: Room;
|
||||
|
|
|
@ -21,7 +21,6 @@ import classNames from 'classnames';
|
|||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import LegacyCallView from "./LegacyCallView";
|
||||
import { RoomViewStore } from '../../../stores/RoomViewStore';
|
||||
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../../../LegacyCallHandler';
|
||||
import PersistentApp from "../elements/PersistentApp";
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
|
@ -34,6 +33,7 @@ import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/Activ
|
|||
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
|
||||
import { SdkContextClass } from '../../../contexts/SDKContext';
|
||||
import { CallStore } from "../../../stores/CallStore";
|
||||
import {
|
||||
VoiceBroadcastRecording,
|
||||
|
@ -129,7 +129,7 @@ class PipView extends React.Component<IProps, IState> {
|
|||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const roomId = RoomViewStore.instance.getRoomId();
|
||||
const roomId = SdkContextClass.instance.roomViewStore.getRoomId();
|
||||
|
||||
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(roomId);
|
||||
|
||||
|
@ -147,7 +147,7 @@ class PipView extends React.Component<IProps, IState> {
|
|||
public componentDidMount() {
|
||||
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallState, this.updateCalls);
|
||||
RoomViewStore.instance.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
SdkContextClass.instance.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
const room = MatrixClientPeg.get()?.getRoom(this.state.viewedRoomId);
|
||||
if (room) {
|
||||
|
@ -164,7 +164,7 @@ class PipView extends React.Component<IProps, IState> {
|
|||
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.updateCalls);
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli?.removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
RoomViewStore.instance.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
SdkContextClass.instance.roomViewStore.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
const room = cli?.getRoom(this.state.viewedRoomId);
|
||||
if (room) {
|
||||
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(room), this.updateCalls);
|
||||
|
@ -186,7 +186,7 @@ class PipView extends React.Component<IProps, IState> {
|
|||
private onMove = () => this.movePersistedElement.current?.();
|
||||
|
||||
private onRoomViewStoreUpdate = () => {
|
||||
const newRoomId = RoomViewStore.instance.getRoomId();
|
||||
const newRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
|
||||
const oldRoomId = this.state.viewedRoomId;
|
||||
if (newRoomId === oldRoomId) return;
|
||||
// The WidgetLayoutStore observer always tracks the currently viewed Room,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue