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
parent 84f2974b57
commit e946674df3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 467 additions and 275 deletions

View file

@ -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<MatrixClient>;
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<ReactWrapper> => {
if (RoomViewStore.instance.getRoomId() !== room.roomId) {
if (stores.roomViewStore.getRoomId() !== room.roomId) {
const switchedRoom = new Promise<void>(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<ViewRoomPayload>({
@ -94,15 +96,16 @@ describe("RoomView", () => {
}
const roomView = mount(
<RoomView
mxClient={cli}
threepidInvite={null}
oobData={null}
resizeNotifier={new ResizeNotifier()}
justCreatedOpts={null}
forceTimeline={false}
onRegistered={null}
/>,
<SDKContext.Provider value={stores}>
<RoomView
threepidInvite={null}
oobData={null}
resizeNotifier={new ResizeNotifier()}
justCreatedOpts={null}
forceTimeline={false}
onRegistered={null}
/>
</SDKContext.Provider>,
);
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);
});
});