diff --git a/src/ContentMessages.ts b/src/ContentMessages.ts index d4cf3cc0ab..8135eaab0e 100644 --- a/src/ContentMessages.ts +++ b/src/ContentMessages.ts @@ -43,7 +43,6 @@ import { RoomUpload } from "./models/RoomUpload"; import SettingsStore from "./settings/SettingsStore"; import { decorateStartSendingTime, sendRoundTripMetric } from "./sendTimePerformanceMetrics"; import { TimelineRenderingType } from "./contexts/RoomContext"; -import { RoomViewStore } from "./stores/RoomViewStore"; import { addReplyToMessageContent } from "./utils/Reply"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import UploadFailureDialog from "./components/views/dialogs/UploadFailureDialog"; @@ -51,6 +50,7 @@ import UploadConfirmDialog from "./components/views/dialogs/UploadConfirmDialog" import { createThumbnail } from "./utils/image-media"; import { attachRelation } from "./components/views/rooms/SendMessageComposer"; import { doMaybeLocalRoomAction } from "./utils/local-room"; +import { SdkContextClass } from "./contexts/SDKContext"; // scraped out of a macOS hidpi (5660ppm) screenshot png // 5669 px (x-axis) , 5669 px (y-axis) , per metre @@ -361,7 +361,7 @@ export default class ContentMessages { return; } - const replyToEvent = RoomViewStore.instance.getQuotingEvent(); + const replyToEvent = SdkContextClass.instance.roomViewStore.getQuotingEvent(); if (!this.mediaConfig) { // hot-path optimization to not flash a spinner if we don't need to const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner'); await this.ensureMediaConfigFetched(matrixClient); diff --git a/src/Notifier.ts b/src/Notifier.ts index dd0ebc296a..cc84acb2fa 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -41,12 +41,12 @@ import SettingsStore from "./settings/SettingsStore"; import { hideToast as hideNotificationsToast } from "./toasts/DesktopNotificationsToast"; import { SettingLevel } from "./settings/SettingLevel"; import { isPushNotifyDisabled } from "./settings/controllers/NotificationControllers"; -import { RoomViewStore } from "./stores/RoomViewStore"; import UserActivity from "./UserActivity"; import { mediaFromMxc } from "./customisations/Media"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import LegacyCallHandler from "./LegacyCallHandler"; import VoipUserMapper from "./VoipUserMapper"; +import { SdkContextClass } from "./contexts/SDKContext"; import { localNotificationsAreSilenced } from "./utils/notifications"; import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast"; import ToastStore from "./stores/ToastStore"; @@ -435,7 +435,7 @@ export const Notifier = { if (actions?.notify) { this._performCustomEventHandling(ev); - if (RoomViewStore.instance.getRoomId() === room.roomId && + if (SdkContextClass.instance.roomViewStore.getRoomId() === room.roomId && UserActivity.sharedInstance().userActiveRecently() && !Modal.hasDialogs() ) { diff --git a/src/ScalarMessaging.ts b/src/ScalarMessaging.ts index c511d291ce..72ff94d4d3 100644 --- a/src/ScalarMessaging.ts +++ b/src/ScalarMessaging.ts @@ -272,12 +272,12 @@ import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClientPeg } from './MatrixClientPeg'; import dis from './dispatcher/dispatcher'; import WidgetUtils from './utils/WidgetUtils'; -import { RoomViewStore } from './stores/RoomViewStore'; import { _t } from './languageHandler'; import { IntegrationManagers } from "./integrations/IntegrationManagers"; import { WidgetType } from "./widgets/WidgetType"; import { objectClone } from "./utils/objects"; import { EffectiveMembership, getEffectiveMembership } from './utils/membership'; +import { SdkContextClass } from './contexts/SDKContext'; enum Action { CloseScalar = "close_scalar", @@ -721,7 +721,7 @@ const onMessage = function(event: MessageEvent): void { } } - if (roomId !== RoomViewStore.instance.getRoomId()) { + if (roomId !== SdkContextClass.instance.roomViewStore.getRoomId()) { sendError(event, _t('Room %(roomId)s not visible', { roomId: roomId })); return; } diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index bbd936ce75..624c515b15 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -62,7 +62,6 @@ import InfoDialog from "./components/views/dialogs/InfoDialog"; import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog"; import { shouldShowComponent } from "./customisations/helpers/UIComponents"; import { TimelineRenderingType } from './contexts/RoomContext'; -import { RoomViewStore } from "./stores/RoomViewStore"; import { XOR } from "./@types/common"; import { PosthogAnalytics } from "./PosthogAnalytics"; import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload"; @@ -70,6 +69,7 @@ import VoipUserMapper from './VoipUserMapper'; import { htmlSerializeFromMdIfNeeded } from './editor/serialize'; import { leaveRoomBehaviour } from "./utils/leave-behaviour"; import { isLocalRoom } from './utils/localRoom/isLocalRoom'; +import { SdkContextClass } from './contexts/SDKContext'; // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 interface HTMLInputEvent extends Event { @@ -209,7 +209,7 @@ function successSync(value: any) { const isCurrentLocalRoom = (): boolean => { const cli = MatrixClientPeg.get(); - const room = cli.getRoom(RoomViewStore.instance.getRoomId()); + const room = cli.getRoom(SdkContextClass.instance.roomViewStore.getRoomId()); return isLocalRoom(room); }; @@ -868,7 +868,7 @@ export const Commands = [ description: _td('Define the power level of a user'), isEnabled(): boolean { const cli = MatrixClientPeg.get(); - const room = cli.getRoom(RoomViewStore.instance.getRoomId()); + const room = cli.getRoom(SdkContextClass.instance.roomViewStore.getRoomId()); return room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getUserId()) && !isLocalRoom(room); }, @@ -909,7 +909,7 @@ export const Commands = [ description: _td('Deops user with given id'), isEnabled(): boolean { const cli = MatrixClientPeg.get(); - const room = cli.getRoom(RoomViewStore.instance.getRoomId()); + const room = cli.getRoom(SdkContextClass.instance.roomViewStore.getRoomId()); return room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getUserId()) && !isLocalRoom(room); }, diff --git a/src/audio/PlaybackQueue.ts b/src/audio/PlaybackQueue.ts index 72ed8cf169..c5a6ee64f2 100644 --- a/src/audio/PlaybackQueue.ts +++ b/src/audio/PlaybackQueue.ts @@ -25,7 +25,7 @@ import { MatrixClientPeg } from "../MatrixClientPeg"; import { arrayFastClone } from "../utils/arrays"; import { PlaybackManager } from "./PlaybackManager"; import { isVoiceMessage } from "../utils/EventUtils"; -import { RoomViewStore } from "../stores/RoomViewStore"; +import { SdkContextClass } from "../contexts/SDKContext"; /** * Audio playback queue management for a given room. This keeps track of where the user @@ -51,7 +51,7 @@ export class PlaybackQueue { constructor(private room: Room) { this.loadClocks(); - RoomViewStore.instance.addRoomListener(this.room.roomId, (isActive) => { + SdkContextClass.instance.roomViewStore.addRoomListener(this.room.roomId, (isActive) => { if (!isActive) return; // Reset the state of the playbacks before they start mounting and enqueuing updates. diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 515355b63d..06a73ff605 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -137,6 +137,7 @@ 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'; // legacy export @@ -238,9 +239,12 @@ export default class MatrixChat extends React.PureComponent { 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, @@ -762,6 +766,7 @@ export default class MatrixChat extends React.PureComponent { 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 && @@ -2087,7 +2092,9 @@ export default class MatrixChat extends React.PureComponent { } return - { view } + + { view } + ; } } diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 6425709ea7..77245e0eb8 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -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"; @@ -120,6 +115,7 @@ 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 +129,7 @@ if (DEBUG) { debuglog = logger.log.bind(console); } -interface IRoomProps extends MatrixClientProps { +interface IRoomProps { threepidInvite: IThreepidInvite; oobData?: IOOBData; @@ -381,13 +377,13 @@ export class RoomView extends React.Component { private messagePanel: TimelinePanel; private roomViewBody = createRef(); - static contextType = MatrixClientContext; - public context!: React.ContextType; + static contextType = SDKContext; + public context!: React.ContextType; - constructor(props: IRoomProps, context: React.ContextType) { + constructor(props: IRoomProps, context: React.ContextType) { super(props, context); - const llMembers = context.hasLazyLoadMembersEnabled(); + const llMembers = context.client.hasLazyLoadMembersEnabled(); this.state = { roomId: null, roomLoading: true, @@ -422,7 +418,7 @@ export class RoomView extends React.Component { showJoinLeaves: true, showAvatarChanges: true, showDisplaynameChanges: true, - matrixClientIsReady: context?.isInitialSyncComplete(), + matrixClientIsReady: context.client?.isInitialSyncComplete(), mainSplitContentType: MainSplitContentType.Timeline, timelineRenderingType: TimelineRenderingType.Room, liveTimeline: undefined, @@ -430,25 +426,25 @@ export class RoomView extends React.Component { }; 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 +497,16 @@ export class RoomView extends React.Component { 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 +514,12 @@ export class RoomView extends React.Component { 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 +530,7 @@ export class RoomView extends React.Component { 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 +545,45 @@ export class RoomView extends React.Component { 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 & Pick = { 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 +596,7 @@ export class RoomView extends React.Component { // becomes available to fetch a whole thread if (!initialEvent) { initialEvent = await fetchInitialEvent( - this.context, + this.context.client, roomId, initialEventId, ); @@ -616,21 +612,21 @@ export class RoomView extends React.Component { 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({ 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 +653,7 @@ export class RoomView extends React.Component { 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 +670,7 @@ export class RoomView extends React.Component { // 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 +780,7 @@ export class RoomView extends React.Component { 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 +813,7 @@ export class RoomView extends React.Component { }); } 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 +831,7 @@ export class RoomView extends React.Component { // 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 +844,7 @@ export class RoomView extends React.Component { callState: callState, }); - LegacyCallHandler.instance.on(LegacyCallHandlerEvent.CallState, this.onCallState); + this.context.legacyCallHandler.on(LegacyCallHandlerEvent.CallState, this.onCallState); window.addEventListener('beforeunload', this.onPageUnload); } @@ -885,7 +881,7 @@ export class RoomView extends React.Component { // (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 +889,47 @@ export class RoomView extends React.Component { } 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 +940,13 @@ export class RoomView extends React.Component { 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 +1013,7 @@ export class RoomView extends React.Component { 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 +1039,7 @@ export class RoomView extends React.Component { 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 +1108,7 @@ export class RoomView extends React.Component { 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 +1141,7 @@ export class RoomView extends React.Component { 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 +1161,7 @@ export class RoomView extends React.Component { }; 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 +1198,7 @@ export class RoomView extends React.Component { 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 +1210,10 @@ export class RoomView extends React.Component { 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 +1240,7 @@ export class RoomView extends React.Component { 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 +1266,7 @@ export class RoomView extends React.Component { 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 +1279,7 @@ export class RoomView extends React.Component { // 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 +1316,15 @@ export class RoomView extends React.Component { }; 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,7 +1370,7 @@ export class RoomView extends React.Component { 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) @@ -1442,7 +1438,7 @@ export class RoomView extends React.Component { 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>({ @@ -1499,13 +1495,13 @@ export class RoomView extends React.Component { }; 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 +1574,7 @@ export class RoomView extends React.Component { 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 +1582,7 @@ export class RoomView extends React.Component { const bundledRelationship = event .getServerAggregatedRelation(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 +1654,7 @@ export class RoomView extends React.Component { 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 +1711,7 @@ export class RoomView extends React.Component { 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 +1738,13 @@ export class RoomView extends React.Component { }); 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 +1907,7 @@ export class RoomView extends React.Component { 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 +1920,7 @@ export class RoomView extends React.Component { 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 +1949,7 @@ export class RoomView extends React.Component { Array.from(dataTransfer.files), this.state.room?.roomId ?? this.state.roomId, null, - this.context, + this.context.client, TimelineRenderingType.Room, ); @@ -1970,7 +1966,7 @@ export class RoomView extends React.Component { } private renderLocalRoomCreateLoader(): ReactElement { - const names = this.state.room.getDefaultRoomName(this.props.mxClient.getUserId()); + const names = this.state.room.getDefaultRoomName(this.context.client.getUserId()); return { ); } 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 +2158,7 @@ export class RoomView extends React.Component { 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 +2170,7 @@ export class RoomView extends React.Component { 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 = ; @@ -2236,7 +2232,7 @@ export class RoomView extends React.Component { const auxPanel = ( @@ -2397,7 +2393,7 @@ export class RoomView extends React.Component { mainSplitBody = <> @@ -2451,7 +2447,7 @@ export class RoomView extends React.Component { 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 +2489,4 @@ export class RoomView extends React.Component { } } -const RoomViewWithMatrixClient = withMatrixClientHOC(RoomView); -export default RoomViewWithMatrixClient; +export default RoomView; diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index 7336dfeb0c..00ebfdacce 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -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; diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index 042b8b3b92..a7b4ab10c8 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -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; @@ -113,7 +113,7 @@ export default class ThreadView extends React.Component { 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({ action: Action.ViewRoom, diff --git a/src/components/views/beacon/RoomCallBanner.tsx b/src/components/views/beacon/RoomCallBanner.tsx index 736c88649f..6085fe141b 100644 --- a/src/components/views/beacon/RoomCallBanner.tsx +++ b/src/components/views/beacon/RoomCallBanner.tsx @@ -24,13 +24,13 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { Action } from "../../../dispatcher/actions"; import { Call, ConnectionState, ElementCall } from "../../../models/Call"; import { useCall } from "../../../hooks/useCall"; -import { RoomViewStore } from "../../../stores/RoomViewStore"; 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"]; @@ -114,7 +114,7 @@ const RoomCallBanner: React.FC = ({ roomId }) => { } // Check if the call is already showing. No banner is needed in this case. - if (RoomViewStore.instance.isViewingCall()) { + if (SdkContextClass.instance.roomViewStore.isViewingCall()) { return null; } diff --git a/src/components/views/context_menus/RoomContextMenu.tsx b/src/components/views/context_menus/RoomContextMenu.tsx index b9923d9278..aadfd2d268 100644 --- a/src/components/views/context_menus/RoomContextMenu.tsx +++ b/src/components/views/context_menus/RoomContextMenu.tsx @@ -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({ 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(); }} diff --git a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index b04299869c..dfec2ab509 100644 --- a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx @@ -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 = ({ initialText = "", initialFilter = n
{ BreadcrumbsStore.instance.rooms - .filter(r => r.roomId !== RoomViewStore.instance.getRoomId()) + .filter(r => r.roomId !== SdkContextClass.instance.roomViewStore.getRoomId()) .map(room => ( { ); 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)) { diff --git a/src/components/views/right_panel/TimelineCard.tsx b/src/components/views/right_panel/TimelineCard.tsx index f1eea5ad49..c88a47406a 100644 --- a/src/components/views/right_panel/TimelineCard.tsx +++ b/src/components/views/right_panel/TimelineCard.tsx @@ -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 { } 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 { } 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 { private onRoomViewStoreUpdate = async (initial?: boolean): Promise => { const newState: Pick = { - // 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); diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 810ae48dd7..49201d52bc 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -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. diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 13b1011088..0cd38f7b30 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -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 { 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 { 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({ diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 219295d23d..68f4dfe4de 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -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 { 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 { 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 { } 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, diff --git a/src/components/views/spaces/QuickSettingsButton.tsx b/src/components/views/spaces/QuickSettingsButton.tsx index 2f4ee9315f..eb7244f994 100644 --- a/src/components/views/spaces/QuickSettingsButton.tsx +++ b/src/components/views/spaces/QuickSettingsButton.tsx @@ -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(); @@ -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" diff --git a/src/components/views/voip/PipView.tsx b/src/components/views/voip/PipView.tsx index 0bebfe1bf3..3aaa9ac430 100644 --- a/src/components/views/voip/PipView.tsx +++ b/src/components/views/voip/PipView.tsx @@ -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 { 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 { 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 { 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 { 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, diff --git a/src/contexts/SDKContext.ts b/src/contexts/SDKContext.ts new file mode 100644 index 0000000000..61905dca92 --- /dev/null +++ b/src/contexts/SDKContext.ts @@ -0,0 +1,127 @@ +/* +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 { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { createContext } from "react"; + +import defaultDispatcher from "../dispatcher/dispatcher"; +import LegacyCallHandler from "../LegacyCallHandler"; +import { PosthogAnalytics } from "../PosthogAnalytics"; +import { SlidingSyncManager } from "../SlidingSyncManager"; +import { RoomNotificationStateStore } from "../stores/notifications/RoomNotificationStateStore"; +import RightPanelStore from "../stores/right-panel/RightPanelStore"; +import { RoomViewStore } from "../stores/RoomViewStore"; +import SpaceStore, { SpaceStoreClass } from "../stores/spaces/SpaceStore"; +import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; +import WidgetStore from "../stores/WidgetStore"; + +export const SDKContext = createContext(undefined); +SDKContext.displayName = "SDKContext"; + +/** + * A class which lazily initialises stores as and when they are requested, ensuring they remain + * as singletons scoped to this object. + */ +export class SdkContextClass { + /** + * The global SdkContextClass instance. This is a temporary measure whilst so many stores remain global + * as well. Over time, these stores should accept a `SdkContextClass` instance in their constructor. + * When all stores do this, this static variable can be deleted. + */ + public static readonly instance = new SdkContextClass(); + + // Optional as we don't have a client on initial load if unregistered. This should be set + // when the MatrixClient is first acquired in the dispatcher event Action.OnLoggedIn. + // It is only safe to set this once, as updating this value will NOT notify components using + // this Context. + public client?: MatrixClient; + + // All protected fields to make it easier to derive test stores + protected _RightPanelStore?: RightPanelStore; + protected _RoomNotificationStateStore?: RoomNotificationStateStore; + protected _RoomViewStore?: RoomViewStore; + protected _WidgetLayoutStore?: WidgetLayoutStore; + protected _WidgetStore?: WidgetStore; + protected _PosthogAnalytics?: PosthogAnalytics; + protected _SlidingSyncManager?: SlidingSyncManager; + protected _SpaceStore?: SpaceStoreClass; + protected _LegacyCallHandler?: LegacyCallHandler; + + /** + * Automatically construct stores which need to be created eagerly so they can register with + * the dispatcher. + */ + public constructEagerStores() { + this._RoomViewStore = this.roomViewStore; + } + + public get legacyCallHandler(): LegacyCallHandler { + if (!this._LegacyCallHandler) { + this._LegacyCallHandler = LegacyCallHandler.instance; + } + return this._LegacyCallHandler; + } + public get rightPanelStore(): RightPanelStore { + if (!this._RightPanelStore) { + this._RightPanelStore = RightPanelStore.instance; + } + return this._RightPanelStore; + } + public get roomNotificationStateStore(): RoomNotificationStateStore { + if (!this._RoomNotificationStateStore) { + this._RoomNotificationStateStore = RoomNotificationStateStore.instance; + } + return this._RoomNotificationStateStore; + } + public get roomViewStore(): RoomViewStore { + if (!this._RoomViewStore) { + this._RoomViewStore = new RoomViewStore( + defaultDispatcher, this, + ); + } + return this._RoomViewStore; + } + public get widgetLayoutStore(): WidgetLayoutStore { + if (!this._WidgetLayoutStore) { + this._WidgetLayoutStore = WidgetLayoutStore.instance; + } + return this._WidgetLayoutStore; + } + public get widgetStore(): WidgetStore { + if (!this._WidgetStore) { + this._WidgetStore = WidgetStore.instance; + } + return this._WidgetStore; + } + public get posthogAnalytics(): PosthogAnalytics { + if (!this._PosthogAnalytics) { + this._PosthogAnalytics = PosthogAnalytics.instance; + } + return this._PosthogAnalytics; + } + public get slidingSyncManager(): SlidingSyncManager { + if (!this._SlidingSyncManager) { + this._SlidingSyncManager = SlidingSyncManager.instance; + } + return this._SlidingSyncManager; + } + public get spaceStore(): SpaceStoreClass { + if (!this._SpaceStore) { + this._SpaceStore = SpaceStore.instance; + } + return this._SpaceStore; + } +} diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index 0a15ce1860..b3814f7a32 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -17,6 +17,7 @@ limitations under the License. */ import React, { ReactNode } from "react"; +import * as utils from 'matrix-js-sdk/src/utils'; import { MatrixError } from "matrix-js-sdk/src/http-api"; import { logger } from "matrix-js-sdk/src/logger"; import { ViewRoom as ViewRoomEvent } from "@matrix-org/analytics-events/types/typescript/ViewRoom"; @@ -27,7 +28,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { Optional } from "matrix-events-sdk"; import EventEmitter from "events"; -import { defaultDispatcher, MatrixDispatcher } from '../dispatcher/dispatcher'; +import { MatrixDispatcher } from '../dispatcher/dispatcher'; import { MatrixClientPeg } from '../MatrixClientPeg'; import Modal from '../Modal'; import { _t } from '../languageHandler'; @@ -35,10 +36,8 @@ import { getCachedRoomIDForAlias, storeRoomAliasInCache } from '../RoomAliasCach import { Action } from "../dispatcher/actions"; import { retry } from "../utils/promise"; import { TimelineRenderingType } from "../contexts/RoomContext"; -import { PosthogAnalytics } from "../PosthogAnalytics"; import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload"; import DMRoomMap from "../utils/DMRoomMap"; -import SpaceStore from "./spaces/SpaceStore"; import { isMetaSpace, MetaSpace } from "./spaces"; import { JoinRoomPayload } from "../dispatcher/payloads/JoinRoomPayload"; import { JoinRoomReadyPayload } from "../dispatcher/payloads/JoinRoomReadyPayload"; @@ -47,9 +46,9 @@ import { ViewRoomErrorPayload } from "../dispatcher/payloads/ViewRoomErrorPayloa import ErrorDialog from "../components/views/dialogs/ErrorDialog"; import { ActiveRoomChangedPayload } from "../dispatcher/payloads/ActiveRoomChangedPayload"; import SettingsStore from "../settings/SettingsStore"; -import { SlidingSyncManager } from "../SlidingSyncManager"; import { awaitRoomDownSync } from "../utils/RoomUpgrade"; import { UPDATE_EVENT } from "./AsyncStore"; +import { SdkContextClass } from "../contexts/SDKContext"; import { CallStore } from "./CallStore"; const NUM_JOIN_RETRY = 5; @@ -131,17 +130,16 @@ type Listener = (isActive: boolean) => void; * A class for storing application state for RoomView. */ export class RoomViewStore extends EventEmitter { - // Important: This cannot be a dynamic getter (lazily-constructed instance) because - // otherwise we'll miss view_room dispatches during startup, breaking relaunches of - // the app. We need to eagerly create the instance. - public static readonly instance = new RoomViewStore(defaultDispatcher); - - private state: State = INITIAL_STATE; // initialize state + // initialize state as a copy of the initial state. We need to copy else one RVS can talk to + // another RVS via INITIAL_STATE as they share the same underlying object. Mostly relevant for tests. + private state = utils.deepCopy(INITIAL_STATE); private dis: MatrixDispatcher; private dispatchToken: string; - public constructor(dis: MatrixDispatcher) { + public constructor( + dis: MatrixDispatcher, private readonly stores: SdkContextClass, + ) { super(); this.resetDispatcher(dis); } @@ -248,7 +246,7 @@ export class RoomViewStore extends EventEmitter { : numMembers > 1 ? "Two" : "One"; - PosthogAnalytics.instance.trackEvent({ + this.stores.posthogAnalytics.trackEvent({ eventName: "JoinedRoom", trigger: payload.metricsTrigger, roomSize, @@ -291,17 +289,17 @@ export class RoomViewStore extends EventEmitter { if (payload.metricsTrigger !== null && payload.room_id !== this.state.roomId) { let activeSpace: ViewRoomEvent["activeSpace"]; - if (SpaceStore.instance.activeSpace === MetaSpace.Home) { + if (this.stores.spaceStore.activeSpace === MetaSpace.Home) { activeSpace = "Home"; - } else if (isMetaSpace(SpaceStore.instance.activeSpace)) { + } else if (isMetaSpace(this.stores.spaceStore.activeSpace)) { activeSpace = "Meta"; } else { - activeSpace = SpaceStore.instance.activeSpaceRoom.getJoinRule() === JoinRule.Public + activeSpace = this.stores.spaceStore.activeSpaceRoom?.getJoinRule() === JoinRule.Public ? "Public" : "Private"; } - PosthogAnalytics.instance.trackEvent({ + this.stores.posthogAnalytics.trackEvent({ eventName: "ViewRoom", trigger: payload.metricsTrigger, viaKeyboard: payload.metricsViaKeyboard, @@ -314,7 +312,7 @@ export class RoomViewStore extends EventEmitter { if (SettingsStore.getValue("feature_sliding_sync") && this.state.roomId !== payload.room_id) { if (this.state.subscribingRoomId && this.state.subscribingRoomId !== payload.room_id) { // unsubscribe from this room, but don't await it as we don't care when this gets done. - SlidingSyncManager.instance.setRoomVisible(this.state.subscribingRoomId, false); + this.stores.slidingSyncManager.setRoomVisible(this.state.subscribingRoomId, false); } this.setState({ subscribingRoomId: payload.room_id, @@ -332,11 +330,11 @@ export class RoomViewStore extends EventEmitter { }); // set this room as the room subscription. We need to await for it as this will fetch // all room state for this room, which is required before we get the state below. - await SlidingSyncManager.instance.setRoomVisible(payload.room_id, true); + await this.stores.slidingSyncManager.setRoomVisible(payload.room_id, true); // Whilst we were subscribing another room was viewed, so stop what we're doing and // unsubscribe if (this.state.subscribingRoomId !== payload.room_id) { - SlidingSyncManager.instance.setRoomVisible(payload.room_id, false); + this.stores.slidingSyncManager.setRoomVisible(payload.room_id, false); return; } // Re-fire the payload: we won't re-process it because the prev room ID == payload room ID now @@ -599,7 +597,7 @@ export class RoomViewStore extends EventEmitter { // // Not joined // } // } else { - // if (RoomViewStore.instance.isJoining()) { + // if (this.stores.roomViewStore.isJoining()) { // // show spinner // } else { // // show join prompt diff --git a/src/stores/right-panel/RightPanelStore.ts b/src/stores/right-panel/RightPanelStore.ts index 327f82153f..9aa4c1b27c 100644 --- a/src/stores/right-panel/RightPanelStore.ts +++ b/src/stores/right-panel/RightPanelStore.ts @@ -34,7 +34,7 @@ import { import { ActionPayload } from "../../dispatcher/payloads"; import { Action } from "../../dispatcher/actions"; import { ActiveRoomChangedPayload } from "../../dispatcher/payloads/ActiveRoomChangedPayload"; -import { RoomViewStore } from "../RoomViewStore"; +import { SdkContextClass } from "../../contexts/SDKContext"; /** * A class for tracking the state of the right panel between layouts and @@ -64,7 +64,7 @@ export default class RightPanelStore extends ReadyWatchingStore { } protected async onReady(): Promise { - this.viewedRoomId = RoomViewStore.instance.getRoomId(); + this.viewedRoomId = SdkContextClass.instance.roomViewStore.getRoomId(); this.matrixClient.on(CryptoEvent.VerificationRequest, this.onVerificationRequestUpdate); this.loadCacheFromSettings(); this.emitAndUpdateSettings(); diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 83c79a16a9..d6f9de79c3 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -27,7 +27,6 @@ import { ActionPayload } from "../../dispatcher/payloads"; import defaultDispatcher from "../../dispatcher/dispatcher"; import { readReceiptChangeIsFor } from "../../utils/read-receipts"; import { FILTER_CHANGED, IFilterCondition } from "./filters/IFilterCondition"; -import { RoomViewStore } from "../RoomViewStore"; import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/Algorithm"; import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership"; import RoomListLayoutStore from "./RoomListLayoutStore"; @@ -40,6 +39,7 @@ import { IRoomTimelineActionPayload } from "../../actions/MatrixActionCreators"; import { RoomListStore as Interface, RoomListStoreEvent } from "./Interface"; import { SlidingRoomListStoreClass } from "./SlidingRoomListStore"; import { UPDATE_EVENT } from "../AsyncStore"; +import { SdkContextClass } from "../../contexts/SDKContext"; interface IState { // state is tracked in underlying classes @@ -105,7 +105,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient implements this.readyStore.useUnitTestClient(forcedClient); } - RoomViewStore.instance.addListener(UPDATE_EVENT, () => this.handleRVSUpdate({})); + SdkContextClass.instance.roomViewStore.addListener(UPDATE_EVENT, () => this.handleRVSUpdate({})); this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated); this.algorithm.on(FILTER_CHANGED, this.onAlgorithmFilterUpdated); this.setupWatchers(); @@ -128,7 +128,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient implements private handleRVSUpdate({ trigger = true }) { if (!this.matrixClient) return; // We assume there won't be RVS updates without a client - const activeRoomId = RoomViewStore.instance.getRoomId(); + const activeRoomId = SdkContextClass.instance.roomViewStore.getRoomId(); if (!activeRoomId && this.algorithm.stickyRoom) { this.algorithm.setStickyRoom(null); } else if (activeRoomId) { diff --git a/src/stores/room-list/SlidingRoomListStore.ts b/src/stores/room-list/SlidingRoomListStore.ts index 3d532fe0c9..35550d04f1 100644 --- a/src/stores/room-list/SlidingRoomListStore.ts +++ b/src/stores/room-list/SlidingRoomListStore.ts @@ -29,8 +29,8 @@ import { SlidingSyncManager } from "../../SlidingSyncManager"; import SpaceStore from "../spaces/SpaceStore"; import { MetaSpace, SpaceKey, UPDATE_SELECTED_SPACE } from "../spaces"; import { LISTS_LOADING_EVENT } from "./RoomListStore"; -import { RoomViewStore } from "../RoomViewStore"; import { UPDATE_EVENT } from "../AsyncStore"; +import { SdkContextClass } from "../../contexts/SDKContext"; interface IState { // state is tracked in underlying classes @@ -207,7 +207,7 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl // this room will not move due to it being viewed: it is sticky. This can be null to indicate // no sticky room if you aren't viewing a room. - this.stickyRoomId = RoomViewStore.instance.getRoomId(); + this.stickyRoomId = SdkContextClass.instance.roomViewStore.getRoomId(); let stickyRoomNewIndex = -1; const stickyRoomOldIndex = (tagMap[tagId] || []).findIndex((room) => { return room.roomId === this.stickyRoomId; @@ -273,7 +273,7 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl private onRoomViewStoreUpdated() { // we only care about this to know when the user has clicked on a room to set the stickiness value - if (RoomViewStore.instance.getRoomId() === this.stickyRoomId) { + if (SdkContextClass.instance.roomViewStore.getRoomId() === this.stickyRoomId) { return; } @@ -303,7 +303,7 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl } } // in the event we didn't call refreshOrderedLists, it helps to still remember the sticky room ID. - this.stickyRoomId = RoomViewStore.instance.getRoomId(); + this.stickyRoomId = SdkContextClass.instance.roomViewStore.getRoomId(); if (hasUpdatedAnyList) { this.emit(LISTS_UPDATE_EVENT); @@ -314,7 +314,7 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient impl logger.info("SlidingRoomListStore.onReady"); // permanent listeners: never get destroyed. Could be an issue if we want to test this in isolation. SlidingSyncManager.instance.slidingSync.on(SlidingSyncEvent.List, this.onSlidingSyncListUpdate.bind(this)); - RoomViewStore.instance.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdated.bind(this)); + SdkContextClass.instance.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdated.bind(this)); SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated.bind(this)); if (SpaceStore.instance.activeSpace) { this.onSelectedSpaceUpdated(SpaceStore.instance.activeSpace, false); diff --git a/src/stores/spaces/SpaceStore.ts b/src/stores/spaces/SpaceStore.ts index ce86b6ec0f..f4802a1520 100644 --- a/src/stores/spaces/SpaceStore.ts +++ b/src/stores/spaces/SpaceStore.ts @@ -34,7 +34,6 @@ import { RoomNotificationStateStore } from "../notifications/RoomNotificationSta import { DefaultTagID } from "../room-list/models"; import { EnhancedMap, mapDiff } from "../../utils/maps"; import { setDiff, setHasDiff } from "../../utils/sets"; -import { RoomViewStore } from "../RoomViewStore"; import { Action } from "../../dispatcher/actions"; import { arrayHasDiff, arrayHasOrderChange } from "../../utils/arrays"; import { reorderLexicographically } from "../../utils/stringOrderField"; @@ -64,6 +63,7 @@ import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload"; import { SwitchSpacePayload } from "../../dispatcher/payloads/SwitchSpacePayload"; import { AfterLeaveRoomPayload } from "../../dispatcher/payloads/AfterLeaveRoomPayload"; +import { SdkContextClass } from "../../contexts/SDKContext"; interface IState { } @@ -797,7 +797,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.updateNotificationStates(notificationStatesToUpdate); }; - private switchSpaceIfNeeded = (roomId = RoomViewStore.instance.getRoomId()) => { + private switchSpaceIfNeeded = (roomId = SdkContextClass.instance.roomViewStore.getRoomId()) => { if (!this.isRoomInSpace(this.activeSpace, roomId) && !this.matrixClient.getRoom(roomId)?.isSpaceRoom()) { this.switchToRelatedSpace(roomId); } @@ -848,7 +848,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } // if the room currently being viewed was just joined then switch to its related space - if (newMembership === "join" && room.roomId === RoomViewStore.instance.getRoomId()) { + if (newMembership === "join" && room.roomId === SdkContextClass.instance.roomViewStore.getRoomId()) { this.switchSpaceIfNeeded(room.roomId); } } @@ -875,7 +875,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.emit(room.roomId); } - if (membership === "join" && room.roomId === RoomViewStore.instance.getRoomId()) { + if (membership === "join" && room.roomId === SdkContextClass.instance.roomViewStore.getRoomId()) { // if the user was looking at the space and then joined: select that space this.setActiveSpace(room.roomId, false); } else if (membership === "leave" && room.roomId === this.activeSpace) { diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 91a262fdca..aa1ad2c393 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -41,7 +41,6 @@ import { ClientEvent } from "matrix-js-sdk/src/client"; import { _t } from "../../languageHandler"; import { StopGapWidgetDriver } from "./StopGapWidgetDriver"; import { WidgetMessagingStore } from "./WidgetMessagingStore"; -import { RoomViewStore } from "../RoomViewStore"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import { OwnProfileStore } from "../OwnProfileStore"; import WidgetUtils from '../../utils/WidgetUtils'; @@ -65,6 +64,7 @@ import { arrayFastClone } from "../../utils/arrays"; import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import Modal from "../../Modal"; import ErrorDialog from "../../components/views/dialogs/ErrorDialog"; +import { SdkContextClass } from "../../contexts/SDKContext"; import { VoiceBroadcastRecordingsStore } from "../../voice-broadcast"; // TODO: Destroy all of this code @@ -185,7 +185,7 @@ export class StopGapWidget extends EventEmitter { if (this.roomId) return this.roomId; - return RoomViewStore.instance.getRoomId(); + return SdkContextClass.instance.roomViewStore.getRoomId(); } public get widgetApi(): ClientWidgetApi { @@ -381,7 +381,7 @@ export class StopGapWidget extends EventEmitter { // noinspection JSIgnoredPromiseFromCall IntegrationManagers.sharedInstance().getPrimaryManager().open( - this.client.getRoom(RoomViewStore.instance.getRoomId()), + this.client.getRoom(SdkContextClass.instance.roomViewStore.getRoomId()), `type_${integType}`, integId, ); diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 752d6d57e6..ba01a10926 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -53,9 +53,9 @@ import { CHAT_EFFECTS } from "../../effects"; import { containsEmoji } from "../../effects/utils"; import dis from "../../dispatcher/dispatcher"; import SettingsStore from "../../settings/SettingsStore"; -import { RoomViewStore } from "../RoomViewStore"; import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities"; import { navigateToPermalink } from "../../utils/permalinks/navigator"; +import { SdkContextClass } from "../../contexts/SDKContext"; // TODO: Purge this from the universe @@ -210,7 +210,7 @@ export class StopGapWidgetDriver extends WidgetDriver { targetRoomId: string = null, ): Promise { const client = MatrixClientPeg.get(); - const roomId = targetRoomId || RoomViewStore.instance.getRoomId(); + const roomId = targetRoomId || SdkContextClass.instance.roomViewStore.getRoomId(); if (!client || !roomId) throw new Error("Not in a room or not attached to a client"); @@ -291,7 +291,7 @@ export class StopGapWidgetDriver extends WidgetDriver { const targetRooms = roomIds ? (roomIds.includes(Symbols.AnyRoom) ? client.getVisibleRooms() : roomIds.map(r => client.getRoom(r))) - : [client.getRoom(RoomViewStore.instance.getRoomId())]; + : [client.getRoom(SdkContextClass.instance.roomViewStore.getRoomId())]; return targetRooms.filter(r => !!r); } @@ -430,7 +430,7 @@ export class StopGapWidgetDriver extends WidgetDriver { ): Promise { const client = MatrixClientPeg.get(); const dir = direction as Direction; - roomId = roomId ?? RoomViewStore.instance.getRoomId() ?? undefined; + roomId = roomId ?? SdkContextClass.instance.roomViewStore.getRoomId() ?? undefined; if (typeof roomId !== "string") { throw new Error('Error while reading the current room'); diff --git a/src/utils/DialogOpener.ts b/src/utils/DialogOpener.ts index 0e5a3d2b11..82d16962b2 100644 --- a/src/utils/DialogOpener.ts +++ b/src/utils/DialogOpener.ts @@ -20,7 +20,6 @@ import defaultDispatcher from "../dispatcher/dispatcher"; import { ActionPayload } from "../dispatcher/payloads"; import Modal from "../Modal"; import RoomSettingsDialog from "../components/views/dialogs/RoomSettingsDialog"; -import { RoomViewStore } from "../stores/RoomViewStore"; import ForwardDialog from "../components/views/dialogs/ForwardDialog"; import { MatrixClientPeg } from "../MatrixClientPeg"; import { Action } from "../dispatcher/actions"; @@ -32,6 +31,7 @@ import AddExistingToSpaceDialog from "../components/views/dialogs/AddExistingToS import { ButtonEvent } from "../components/views/elements/AccessibleButton"; import PosthogTrackers from "../PosthogTrackers"; import { showAddExistingSubspace, showCreateNewRoom } from "./space"; +import { SdkContextClass } from "../contexts/SDKContext"; /** * Auxiliary class to listen for dialog opening over the dispatcher and @@ -58,7 +58,7 @@ export class DialogOpener { switch (payload.action) { case 'open_room_settings': Modal.createDialog(RoomSettingsDialog, { - roomId: payload.room_id || RoomViewStore.instance.getRoomId(), + roomId: payload.room_id || SdkContextClass.instance.roomViewStore.getRoomId(), initialTabId: payload.initial_tab_id, }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); break; @@ -108,7 +108,7 @@ export class DialogOpener { onAddSubspaceClick: () => showAddExistingSubspace(space), space, onFinished: (added: boolean) => { - if (added && RoomViewStore.instance.getRoomId() === space.roomId) { + if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) { defaultDispatcher.fire(Action.UpdateSpaceHierarchy); } }, diff --git a/src/utils/leave-behaviour.ts b/src/utils/leave-behaviour.ts index a12cd70ebf..83054ce1b4 100644 --- a/src/utils/leave-behaviour.ts +++ b/src/utils/leave-behaviour.ts @@ -27,7 +27,6 @@ import { _t } from "../languageHandler"; import ErrorDialog from "../components/views/dialogs/ErrorDialog"; import { isMetaSpace } from "../stores/spaces"; import SpaceStore from "../stores/spaces/SpaceStore"; -import { RoomViewStore } from "../stores/RoomViewStore"; import dis from "../dispatcher/dispatcher"; import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload"; import { Action } from "../dispatcher/actions"; @@ -35,6 +34,7 @@ import { ViewHomePagePayload } from "../dispatcher/payloads/ViewHomePagePayload" import LeaveSpaceDialog from "../components/views/dialogs/LeaveSpaceDialog"; import { AfterLeaveRoomPayload } from "../dispatcher/payloads/AfterLeaveRoomPayload"; import { bulkSpaceBehaviour } from "./space"; +import { SdkContextClass } from "../contexts/SDKContext"; export async function leaveRoomBehaviour(roomId: string, retry = true, spinner = true) { let spinnerModal: IHandle; @@ -130,7 +130,7 @@ export async function leaveRoomBehaviour(roomId: string, retry = true, spinner = if (!isMetaSpace(SpaceStore.instance.activeSpace) && SpaceStore.instance.activeSpace !== roomId && - RoomViewStore.instance.getRoomId() === roomId + SdkContextClass.instance.roomViewStore.getRoomId() === roomId ) { dis.dispatch({ action: Action.ViewRoom, diff --git a/src/utils/space.tsx b/src/utils/space.tsx index 9e05f0444b..1e30b7235a 100644 --- a/src/utils/space.tsx +++ b/src/utils/space.tsx @@ -30,7 +30,6 @@ import { showRoomInviteDialog } from "../RoomInvite"; import CreateSubspaceDialog from "../components/views/dialogs/CreateSubspaceDialog"; import AddExistingSubspaceDialog from "../components/views/dialogs/AddExistingSubspaceDialog"; import defaultDispatcher from "../dispatcher/dispatcher"; -import { RoomViewStore } from "../stores/RoomViewStore"; import { Action } from "../dispatcher/actions"; import Spinner from "../components/views/elements/Spinner"; import { shouldShowComponent } from "../customisations/helpers/UIComponents"; @@ -38,6 +37,7 @@ import { UIComponent } from "../settings/UIFeature"; import { OpenSpacePreferencesPayload, SpacePreferenceTab } from "../dispatcher/payloads/OpenSpacePreferencesPayload"; import { OpenSpaceSettingsPayload } from "../dispatcher/payloads/OpenSpaceSettingsPayload"; import { OpenAddExistingToSpaceDialogPayload } from "../dispatcher/payloads/OpenAddExistingToSpaceDialogPayload"; +import { SdkContextClass } from "../contexts/SDKContext"; export const shouldShowSpaceSettings = (space: Room) => { const userId = space.client.getUserId(); @@ -113,7 +113,7 @@ export const showAddExistingSubspace = (space: Room): void => { space, onCreateSubspaceClick: () => showCreateNewSubspace(space), onFinished: (added: boolean) => { - if (added && RoomViewStore.instance.getRoomId() === space.roomId) { + if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) { defaultDispatcher.fire(Action.UpdateSpaceHierarchy); } }, @@ -125,7 +125,7 @@ export const showCreateNewSubspace = (space: Room): void => { space, onAddExistingSpaceClick: () => showAddExistingSubspace(space), onFinished: (added: boolean) => { - if (added && RoomViewStore.instance.getRoomId() === space.roomId) { + if (added && SdkContextClass.instance.roomViewStore.getRoomId() === space.roomId) { defaultDispatcher.fire(Action.UpdateSpaceHierarchy); } }, diff --git a/test/SlashCommands-test.tsx b/test/SlashCommands-test.tsx index 39d3986270..c31d6d70c1 100644 --- a/test/SlashCommands-test.tsx +++ b/test/SlashCommands-test.tsx @@ -21,9 +21,9 @@ import { Command, Commands, getCommand } from '../src/SlashCommands'; import { createTestClient } from './test-utils'; import { MatrixClientPeg } from '../src/MatrixClientPeg'; import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from '../src/models/LocalRoom'; -import { RoomViewStore } from '../src/stores/RoomViewStore'; import SettingsStore from '../src/settings/SettingsStore'; import LegacyCallHandler from '../src/LegacyCallHandler'; +import { SdkContextClass } from '../src/contexts/SDKContext'; describe('SlashCommands', () => { let client: MatrixClient; @@ -38,14 +38,14 @@ describe('SlashCommands', () => { }; const setCurrentRoom = (): void => { - mocked(RoomViewStore.instance.getRoomId).mockReturnValue(roomId); + mocked(SdkContextClass.instance.roomViewStore.getRoomId).mockReturnValue(roomId); mocked(client.getRoom).mockImplementation((rId: string): Room => { if (rId === roomId) return room; }); }; const setCurrentLocalRoon = (): void => { - mocked(RoomViewStore.instance.getRoomId).mockReturnValue(localRoomId); + mocked(SdkContextClass.instance.roomViewStore.getRoomId).mockReturnValue(localRoomId); mocked(client.getRoom).mockImplementation((rId: string): Room => { if (rId === localRoomId) return localRoom; }); @@ -60,7 +60,7 @@ describe('SlashCommands', () => { room = new Room(roomId, client, client.getUserId()); localRoom = new LocalRoom(localRoomId, client, client.getUserId()); - jest.spyOn(RoomViewStore.instance, "getRoomId"); + jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId"); }); describe('/topic', () => { diff --git a/test/TestStores.ts b/test/TestStores.ts new file mode 100644 index 0000000000..dbaa51f504 --- /dev/null +++ b/test/TestStores.ts @@ -0,0 +1,44 @@ +/* +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 { SdkContextClass } from "../src/contexts/SDKContext"; +import { PosthogAnalytics } from "../src/PosthogAnalytics"; +import { SlidingSyncManager } from "../src/SlidingSyncManager"; +import { RoomNotificationStateStore } from "../src/stores/notifications/RoomNotificationStateStore"; +import RightPanelStore from "../src/stores/right-panel/RightPanelStore"; +import { RoomViewStore } from "../src/stores/RoomViewStore"; +import { SpaceStoreClass } from "../src/stores/spaces/SpaceStore"; +import { WidgetLayoutStore } from "../src/stores/widgets/WidgetLayoutStore"; +import WidgetStore from "../src/stores/WidgetStore"; + +/** + * A class which provides the same API as Stores but adds additional unsafe setters which can + * replace individual stores. This is useful for tests which need to mock out stores. + */ +export class TestStores extends SdkContextClass { + public _RightPanelStore?: RightPanelStore; + public _RoomNotificationStateStore?: RoomNotificationStateStore; + public _RoomViewStore?: RoomViewStore; + public _WidgetLayoutStore?: WidgetLayoutStore; + public _WidgetStore?: WidgetStore; + public _PosthogAnalytics?: PosthogAnalytics; + public _SlidingSyncManager?: SlidingSyncManager; + public _SpaceStore?: SpaceStoreClass; + + constructor() { + super(); + } +} diff --git a/test/components/structures/RoomView-test.tsx b/test/components/structures/RoomView-test.tsx index dd45c7df09..a4131100c5 100644 --- a/test/components/structures/RoomView-test.tsx +++ b/test/components/structures/RoomView-test.tsx @@ -32,17 +32,16 @@ import { defaultDispatcher } from "../../../src/dispatcher/dispatcher"; import { ViewRoomPayload } from "../../../src/dispatcher/payloads/ViewRoomPayload"; import { RoomView as _RoomView } from "../../../src/components/structures/RoomView"; import ResizeNotifier from "../../../src/utils/ResizeNotifier"; -import { RoomViewStore } from "../../../src/stores/RoomViewStore"; import SettingsStore from "../../../src/settings/SettingsStore"; import { SettingLevel } from "../../../src/settings/SettingLevel"; import DMRoomMap from "../../../src/utils/DMRoomMap"; import { NotificationState } from "../../../src/stores/notifications/NotificationState"; -import RightPanelStore from "../../../src/stores/right-panel/RightPanelStore"; import { RightPanelPhases } from "../../../src/stores/right-panel/RightPanelStorePhases"; import { LocalRoom, LocalRoomState } from "../../../src/models/LocalRoom"; import { DirectoryMember } from "../../../src/utils/direct-messages"; import { createDmLocalRoom } from "../../../src/utils/dm/createDmLocalRoom"; import { UPDATE_EVENT } from "../../../src/stores/AsyncStore"; +import { SdkContextClass, SDKContext } from "../../../src/contexts/SDKContext"; const RoomView = wrapInMatrixClientContext(_RoomView); @@ -50,6 +49,7 @@ describe("RoomView", () => { let cli: MockedObject; let room: Room; let roomCount = 0; + let stores: SdkContextClass; beforeEach(async () => { mockPlatformPeg({ reload: () => {} }); @@ -64,7 +64,9 @@ describe("RoomView", () => { room.on(RoomEvent.TimelineReset, (...args) => cli.emit(RoomEvent.TimelineReset, ...args)); DMRoomMap.makeShared(); - RightPanelStore.instance.useUnitTestClient(cli); + stores = new SdkContextClass(); + stores.client = cli; + stores.rightPanelStore.useUnitTestClient(cli); }); afterEach(async () => { @@ -73,15 +75,15 @@ describe("RoomView", () => { }); const mountRoomView = async (): Promise => { - if (RoomViewStore.instance.getRoomId() !== room.roomId) { + if (stores.roomViewStore.getRoomId() !== room.roomId) { const switchedRoom = new Promise(resolve => { const subFn = () => { - if (RoomViewStore.instance.getRoomId()) { - RoomViewStore.instance.off(UPDATE_EVENT, subFn); + if (stores.roomViewStore.getRoomId()) { + stores.roomViewStore.off(UPDATE_EVENT, subFn); resolve(); } }; - RoomViewStore.instance.on(UPDATE_EVENT, subFn); + stores.roomViewStore.on(UPDATE_EVENT, subFn); }); defaultDispatcher.dispatch({ @@ -94,15 +96,16 @@ describe("RoomView", () => { } const roomView = mount( - , + + + , ); await act(() => Promise.resolve()); // Allow state to settle return roomView; @@ -162,14 +165,14 @@ describe("RoomView", () => { it("normally doesn't open the chat panel", async () => { jest.spyOn(NotificationState.prototype, "isUnread", "get").mockReturnValue(false); await mountRoomView(); - expect(RightPanelStore.instance.isOpen).toEqual(false); + expect(stores.rightPanelStore.isOpen).toEqual(false); }); it("opens the chat panel if there are unread messages", async () => { jest.spyOn(NotificationState.prototype, "isUnread", "get").mockReturnValue(true); await mountRoomView(); - expect(RightPanelStore.instance.isOpen).toEqual(true); - expect(RightPanelStore.instance.currentCard.phase).toEqual(RightPanelPhases.Timeline); + expect(stores.rightPanelStore.isOpen).toEqual(true); + expect(stores.rightPanelStore.currentCard.phase).toEqual(RightPanelPhases.Timeline); }); }); diff --git a/test/components/views/beacon/RoomCallBanner-test.tsx b/test/components/views/beacon/RoomCallBanner-test.tsx index 52d0ed27d3..722c28ff1f 100644 --- a/test/components/views/beacon/RoomCallBanner-test.tsx +++ b/test/components/views/beacon/RoomCallBanner-test.tsx @@ -42,8 +42,8 @@ import RoomCallBanner from "../../../../src/components/views/beacon/RoomCallBann import { CallStore } from "../../../../src/stores/CallStore"; import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; -import { RoomViewStore } from "../../../../src/stores/RoomViewStore"; import { ConnectionState } from "../../../../src/models/Call"; +import { SdkContextClass } from "../../../../src/contexts/SDKContext"; describe("", () => { let client: Mocked; @@ -132,7 +132,8 @@ describe("", () => { }); it("doesn't show banner if the call is shown", async () => { - jest.spyOn(RoomViewStore.instance, 'isViewingCall').mockReturnValue(true); + jest.spyOn(SdkContextClass.instance.roomViewStore, "isViewingCall"); + mocked(SdkContextClass.instance.roomViewStore.isViewingCall).mockReturnValue(true); await renderBanner(); const banner = await screen.queryByText("Video call"); expect(banner).toBeFalsy(); diff --git a/test/stores/RoomViewStore-test.tsx b/test/stores/RoomViewStore-test.ts similarity index 79% rename from test/stores/RoomViewStore-test.tsx rename to test/stores/RoomViewStore-test.ts index 3ea402438d..f6f6bf2cc7 100644 --- a/test/stores/RoomViewStore-test.tsx +++ b/test/stores/RoomViewStore-test.ts @@ -21,10 +21,21 @@ import { Action } from '../../src/dispatcher/actions'; import { getMockClientWithEventEmitter, untilDispatch, untilEmission } from '../test-utils'; import SettingsStore from '../../src/settings/SettingsStore'; import { SlidingSyncManager } from '../../src/SlidingSyncManager'; +import { PosthogAnalytics } from '../../src/PosthogAnalytics'; import { TimelineRenderingType } from '../../src/contexts/RoomContext'; import { MatrixDispatcher } from '../../src/dispatcher/dispatcher'; import { UPDATE_EVENT } from '../../src/stores/AsyncStore'; import { ActiveRoomChangedPayload } from '../../src/dispatcher/payloads/ActiveRoomChangedPayload'; +import { SpaceStoreClass } from '../../src/stores/spaces/SpaceStore'; +import { TestStores } from '../TestStores'; + +// mock out the injected classes +jest.mock('../../src/PosthogAnalytics'); +const MockPosthogAnalytics = >PosthogAnalytics; +jest.mock('../../src/SlidingSyncManager'); +const MockSlidingSyncManager = >SlidingSyncManager; +jest.mock('../../src/stores/spaces/SpaceStore'); +const MockSpaceStore = >SpaceStoreClass; jest.mock('../../src/utils/DMRoomMap', () => { const mock = { @@ -51,6 +62,9 @@ describe('RoomViewStore', function() { isGuest: jest.fn(), }); const room = new Room(roomId, mockClient, userId); + + let roomViewStore: RoomViewStore; + let slidingSyncManager: SlidingSyncManager; let dis: MatrixDispatcher; beforeEach(function() { @@ -60,10 +74,17 @@ describe('RoomViewStore', function() { mockClient.getRoom.mockReturnValue(room); mockClient.isGuest.mockReturnValue(false); - // Reset the state of the store + // Make the RVS to test dis = new MatrixDispatcher(); - RoomViewStore.instance.reset(); - RoomViewStore.instance.resetDispatcher(dis); + slidingSyncManager = new MockSlidingSyncManager(); + const stores = new TestStores(); + stores._SlidingSyncManager = slidingSyncManager; + stores._PosthogAnalytics = new MockPosthogAnalytics(); + stores._SpaceStore = new MockSpaceStore(); + roomViewStore = new RoomViewStore( + dis, stores, + ); + stores._RoomViewStore = roomViewStore; }); it('can be used to view a room by ID and join', async () => { @@ -71,14 +92,14 @@ describe('RoomViewStore', function() { dis.dispatch({ action: Action.JoinRoom }); await untilDispatch(Action.JoinRoomReady, dis); expect(mockClient.joinRoom).toHaveBeenCalledWith(roomId, { viaServers: [] }); - expect(RoomViewStore.instance.isJoining()).toBe(true); + expect(roomViewStore.isJoining()).toBe(true); }); it('can auto-join a room', async () => { dis.dispatch({ action: Action.ViewRoom, room_id: roomId, auto_join: true }); await untilDispatch(Action.JoinRoomReady, dis); expect(mockClient.joinRoom).toHaveBeenCalledWith(roomId, { viaServers: [] }); - expect(RoomViewStore.instance.isJoining()).toBe(true); + expect(roomViewStore.isJoining()).toBe(true); }); it('emits ActiveRoomChanged when the viewed room changes', async () => { @@ -97,7 +118,7 @@ describe('RoomViewStore', function() { it('invokes room activity listeners when the viewed room changes', async () => { const roomId2 = "!roomid:2"; const callback = jest.fn(); - RoomViewStore.instance.addRoomListener(roomId, callback); + roomViewStore.addRoomListener(roomId, callback); dis.dispatch({ action: Action.ViewRoom, room_id: roomId }); await untilDispatch(Action.ActiveRoomChanged, dis) as ActiveRoomChangedPayload; expect(callback).toHaveBeenCalledWith(true); @@ -116,14 +137,14 @@ describe('RoomViewStore', function() { }, dis); // roomId is set to id of the room alias - expect(RoomViewStore.instance.getRoomId()).toBe(roomId); + expect(roomViewStore.getRoomId()).toBe(roomId); // join the room dis.dispatch({ action: Action.JoinRoom }, true); await untilDispatch(Action.JoinRoomReady, dis); - expect(RoomViewStore.instance.isJoining()).toBeTruthy(); + expect(roomViewStore.isJoining()).toBeTruthy(); expect(mockClient.joinRoom).toHaveBeenCalledWith(alias, { viaServers: [] }); }); @@ -134,7 +155,7 @@ describe('RoomViewStore', function() { const payload = await untilDispatch(Action.ViewRoomError, dis); expect(payload.room_id).toBeNull(); expect(payload.room_alias).toEqual(alias); - expect(RoomViewStore.instance.getRoomAlias()).toEqual(alias); + expect(roomViewStore.getRoomAlias()).toEqual(alias); }); it('emits JoinRoomError if joining the room fails', async () => { @@ -143,8 +164,8 @@ describe('RoomViewStore', function() { dis.dispatch({ action: Action.ViewRoom, room_id: roomId }); dis.dispatch({ action: Action.JoinRoom }); await untilDispatch(Action.JoinRoomError, dis); - expect(RoomViewStore.instance.isJoining()).toBe(false); - expect(RoomViewStore.instance.getJoinError()).toEqual(joinErr); + expect(roomViewStore.isJoining()).toBe(false); + expect(roomViewStore.getJoinError()).toEqual(joinErr); }); it('remembers the event being replied to when swapping rooms', async () => { @@ -154,13 +175,13 @@ describe('RoomViewStore', function() { getRoomId: () => roomId, }; dis.dispatch({ action: 'reply_to_event', event: replyToEvent, context: TimelineRenderingType.Room }); - await untilEmission(RoomViewStore.instance, UPDATE_EVENT); - expect(RoomViewStore.instance.getQuotingEvent()).toEqual(replyToEvent); + await untilEmission(roomViewStore, UPDATE_EVENT); + expect(roomViewStore.getQuotingEvent()).toEqual(replyToEvent); // view the same room, should remember the event. // set the highlighed flag to make sure there is a state change so we get an update event dis.dispatch({ action: Action.ViewRoom, room_id: roomId, highlighted: true }); - await untilEmission(RoomViewStore.instance, UPDATE_EVENT); - expect(RoomViewStore.instance.getQuotingEvent()).toEqual(replyToEvent); + await untilEmission(roomViewStore, UPDATE_EVENT); + expect(roomViewStore.getQuotingEvent()).toEqual(replyToEvent); }); it('swaps to the replied event room if it is not the current room', async () => { @@ -172,18 +193,18 @@ describe('RoomViewStore', function() { }; dis.dispatch({ action: 'reply_to_event', event: replyToEvent, context: TimelineRenderingType.Room }); await untilDispatch(Action.ViewRoom, dis); - expect(RoomViewStore.instance.getQuotingEvent()).toEqual(replyToEvent); - expect(RoomViewStore.instance.getRoomId()).toEqual(roomId2); + expect(roomViewStore.getQuotingEvent()).toEqual(replyToEvent); + expect(roomViewStore.getRoomId()).toEqual(roomId2); }); it('removes the roomId on ViewHomePage', async () => { dis.dispatch({ action: Action.ViewRoom, room_id: roomId }); await untilDispatch(Action.ActiveRoomChanged, dis); - expect(RoomViewStore.instance.getRoomId()).toEqual(roomId); + expect(roomViewStore.getRoomId()).toEqual(roomId); dis.dispatch({ action: Action.ViewHomePage }); - await untilEmission(RoomViewStore.instance, UPDATE_EVENT); - expect(RoomViewStore.instance.getRoomId()).toBeNull(); + await untilEmission(roomViewStore, UPDATE_EVENT); + expect(roomViewStore.getRoomId()).toBeNull(); }); describe('Sliding Sync', function() { @@ -191,23 +212,22 @@ describe('RoomViewStore', function() { jest.spyOn(SettingsStore, 'getValue').mockImplementation((settingName, roomId, value) => { return settingName === "feature_sliding_sync"; // this is enabled, everything else is disabled. }); - RoomViewStore.instance.reset(); }); it("subscribes to the room", async () => { - const setRoomVisible = jest.spyOn(SlidingSyncManager.instance, "setRoomVisible").mockReturnValue( + const setRoomVisible = jest.spyOn(slidingSyncManager, "setRoomVisible").mockReturnValue( Promise.resolve(""), ); const subscribedRoomId = "!sub1:localhost"; dis.dispatch({ action: Action.ViewRoom, room_id: subscribedRoomId }); await untilDispatch(Action.ActiveRoomChanged, dis); - expect(RoomViewStore.instance.getRoomId()).toBe(subscribedRoomId); + expect(roomViewStore.getRoomId()).toBe(subscribedRoomId); expect(setRoomVisible).toHaveBeenCalledWith(subscribedRoomId, true); }); // Regression test for an in-the-wild bug where rooms would rapidly switch forever in sliding sync mode it("doesn't get stuck in a loop if you view rooms quickly", async () => { - const setRoomVisible = jest.spyOn(SlidingSyncManager.instance, "setRoomVisible").mockReturnValue( + const setRoomVisible = jest.spyOn(slidingSyncManager, "setRoomVisible").mockReturnValue( Promise.resolve(""), ); const subscribedRoomId = "!sub1:localhost"; diff --git a/test/stores/widgets/StopGapWidgetDriver-test.ts b/test/stores/widgets/StopGapWidgetDriver-test.ts index d1816bdac3..0fd2f18be7 100644 --- a/test/stores/widgets/StopGapWidgetDriver-test.ts +++ b/test/stores/widgets/StopGapWidgetDriver-test.ts @@ -20,8 +20,8 @@ import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; import { Direction, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { Widget, MatrixWidgetType, WidgetKind, WidgetDriver, ITurnServer } from "matrix-widget-api"; +import { SdkContextClass } from "../../../src/contexts/SDKContext"; import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; -import { RoomViewStore } from "../../../src/stores/RoomViewStore"; import { StopGapWidgetDriver } from "../../../src/stores/widgets/StopGapWidgetDriver"; import { stubClient } from "../../test-utils"; @@ -201,7 +201,7 @@ describe("StopGapWidgetDriver", () => { beforeEach(() => { driver = mkDefaultDriver(); }); it('reads related events from the current room', async () => { - jest.spyOn(RoomViewStore.instance, 'getRoomId').mockReturnValue('!this-room-id'); + jest.spyOn(SdkContextClass.instance.roomViewStore, 'getRoomId').mockReturnValue('!this-room-id'); client.relations.mockResolvedValue({ originalEvent: new MatrixEvent(),