Store refactor: use non-global stores in components (#9293)

* Add Stores and StoresContext and use it in MatrixChat and RoomView

Added a new kind of class:
- Add God object `Stores` which will hold refs to all known stores and the `MatrixClient`. This object is NOT a singleton.
- Add `StoresContext` to hold onto a ref of `Stores` for use inside components.

`StoresContext` is created via:
- Create `Stores` in `MatrixChat`, assigning the `MatrixClient` when we have one set. Currently sets the RVS to `RoomViewStore.instance`.
- Wrap `MatrixChat`s `render()` function in a `StoresContext.Provider` so it can be used anywhere.

`StoresContext` is currently only used in `RoomView` via the following changes:
- Remove the HOC, which redundantly set `mxClient` as a prop. We don't need this as `RoomView` was using the client from `this.context`.
- Change the type of context accepted from `MatrixClientContext` to `StoresContext`.
- Modify alllll the places where `this.context` is used to interact with the client and suffix `.client`.
- Modify places where we use `RoomViewStore.instance` and replace them with `this.context.roomViewStore`.

This makes `RoomView` use a non-global instance of RVS.

* Linting

* SDKContext and make client an optional constructor arg

* Move SDKContext to /src/contexts

* Inject all RVS deps

* Linting

* Remove reset calls; deep copy the INITIAL_STATE to avoid test pollution

* DI singletons used in RoomView; DI them in RoomView-test too

* Initial RoomViewStore.instance after all files are imported to avoid cyclical deps

* Lazily init stores to allow for circular dependencies

Rather than stores accepting a list of other stores in their constructors,
which doesn't work when A needs B and B needs A, make new-style stores simply
accept Stores. When a store needs another store, they access it via `Stores`
which then lazily constructs that store if it needs it. This breaks the
circular dependency at constructor time, without needing to introduce
wiring diagrams or any complex DI framework.

* Delete RoomViewStore.instance

Replaced with Stores.instance.roomViewStore

* Linting

* Move OverridableStores to test/TestStores

* Rejig how eager stores get made; don't automatically do it else tests break

* Linting

* Linting and review comments

* Fix new code to use Stores.instance

* s/Stores/SdkContextClass/g

* Update docs

* Remove unused imports

* Update src/stores/RoomViewStore.tsx

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove empty c'tor to make sonar happy

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
kegsay
2022-10-19 13:07:03 +01:00
committed by GitHub
co-authored by Michael Telatynski
parent 84f2974b57
commit e946674df3
36 changed files with 467 additions and 275 deletions
+18 -20
View File
@@ -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<JoinedRoomEvent>({
this.stores.posthogAnalytics.trackEvent<JoinedRoomEvent>({
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<ViewRoomEvent>({
this.stores.posthogAnalytics.trackEvent<ViewRoomEvent>({
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
+2 -2
View File
@@ -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<any> {
this.viewedRoomId = RoomViewStore.instance.getRoomId();
this.viewedRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
this.matrixClient.on(CryptoEvent.VerificationRequest, this.onVerificationRequestUpdate);
this.loadCacheFromSettings();
this.emitAndUpdateSettings();
+3 -3
View File
@@ -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<IState> 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<IState> 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) {
+5 -5
View File
@@ -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<IState> 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<IState> 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<IState> 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<IState> 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);
+4 -4
View File
@@ -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<IState> {
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<IState> {
}
// 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<IState> {
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) {
+3 -3
View File
@@ -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,
);
+4 -4
View File
@@ -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<ISendEventDetails> {
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<IReadEventRelationsResult> {
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');