Merge remote-tracking branch 'upstream/develop' into last-admin-leave-room-warning

This commit is contained in:
Arnei 2022-10-21 14:18:18 +02:00
commit b3e535a7aa
223 changed files with 7406 additions and 2005 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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",

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

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

View file

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

View file

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

View file

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

View file

@ -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 = {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -179,6 +179,7 @@ const EmojiButton: React.FC<IEmojiButtonProps> = ({ addEmoji, menuPosition }) =>
iconClassName="mx_MessageComposer_emoji"
onClick={openMenu}
title={_t("Emoji")}
inputRef={button}
/>
{ contextMenu }

View file

@ -209,7 +209,8 @@ export function ReadReceiptGroup(
onMouseOver={showTooltip}
onMouseLeave={hideTooltip}
onFocus={showTooltip}
onBlur={hideTooltip}>
onBlur={hideTooltip}
>
{ remText }
<span
className="mx_ReadReceiptGroup_container"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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