Remove SpaceStore singleton (#34088)

* Remove dead code

* Remove LegacyCallHandler singleton

Route via SDKContext to cut import cycles

* Remove SpaceStore singleton

Route via SDKContext to cut import cycles

* Fix tests

* Fix tests

* Fix tests

* Fix tests

* Iterate

* Post-merge fixup

* Iterate
This commit is contained in:
Michael Telatynski
2026-07-13 14:31:39 +00:00
committed by GitHub
parent 7206d887f9
commit 825fab6595
46 changed files with 466 additions and 403 deletions
@@ -10,9 +10,9 @@ import React from "react";
import classNames from "classnames";
import type ResizeNotifier from "../../utils/ResizeNotifier";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { type SpaceKey, UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import { RoomListPanel } from "../views/rooms/RoomListPanel";
import { SDKContext } from "../../contexts/SDKContext.ts";
interface IProps {
isMinimized: boolean;
@@ -24,20 +24,23 @@ interface IState {
}
export default class LeftPanel extends React.Component<IProps, IState> {
public constructor(props: IProps) {
super(props);
public static contextType = SDKContext;
declare public context: React.ContextType<typeof SDKContext>;
public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) {
super(props, context);
this.state = {
activeSpace: SpaceStore.instance.activeSpace,
activeSpace: context.spaceStore.activeSpace,
};
}
public componentDidMount(): void {
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
this.context.spaceStore.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
}
public componentWillUnmount(): void {
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
this.context.spaceStore.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
}
private updateActiveSpace = (activeSpace: SpaceKey): void => {
@@ -19,7 +19,6 @@ import { _t, _td } from "../../../languageHandler";
import BaseDialog from "./BaseDialog";
import Dropdown from "../elements/Dropdown";
import SearchBox from "../../structures/SearchBox";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import RoomAvatar from "../avatars/RoomAvatar";
import { getDisplayAliasForRoom } from "../../../Rooms";
import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton";
@@ -35,6 +34,7 @@ import LazyRenderList from "../elements/LazyRenderList";
import { useSettingValue } from "../../../hooks/useSettings";
import { filterBoolean } from "../../../utils/arrays";
import { type NonEmptyArray } from "../../../@types/common";
import { SDKContextClass } from "../../../contexts/SDKContextClass.ts";
// These values match CSS
const ROW_HEIGHT = 32 + 12;
@@ -155,8 +155,14 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase().trim();
const existingSubspacesSet = useMemo(() => new Set(SpaceStore.instance.getChildSpaces(space.roomId)), [space]);
const existingRoomsSet = useMemo(() => new Set(SpaceStore.instance.getChildRooms(space.roomId)), [space]);
const existingSubspacesSet = useMemo(
() => new Set(SDKContextClass.instance.spaceStore.getChildSpaces(space.roomId)),
[space],
);
const existingRoomsSet = useMemo(
() => new Set(SDKContextClass.instance.spaceStore.getChildRooms(space.roomId)),
[space],
);
const [spaces, rooms, dms] = useMemo(() => {
let rooms = visibleRooms;
@@ -201,15 +207,17 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
for (const room of selectedToAdd) {
const via = calculateRoomVia(room);
try {
await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async (e): Promise<void> => {
if (e.errcode === "M_LIMIT_EXCEEDED") {
await sleep(e.data.retry_after_ms);
await SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry
return;
}
await SDKContextClass.instance.spaceStore
.addRoomToSpace(space, room.roomId, via)
.catch(async (e): Promise<void> => {
if (e.errcode === "M_LIMIT_EXCEEDED") {
await sleep(e.data.retry_after_ms);
await SDKContextClass.instance.spaceStore.addRoomToSpace(space, room.roomId, via); // retry
return;
}
throw e;
});
throw e;
});
setProgress((i) => (i ?? 0) + 1);
} catch (e) {
logger.error("Failed to add rooms to space", e);
@@ -398,7 +406,7 @@ export const SubspaceSelector: React.FC<ISubspaceSelectorProps> = ({ title, spac
const options = useMemo(() => {
return [
space,
...SpaceStore.instance.getChildSpaces(space.roomId).filter((space) => {
...SDKContextClass.instance.spaceStore.getChildSpaces(space.roomId).filter((space) => {
return space.currentState.maySendStateEvent(EventType.SpaceChild, space.client.getSafeUserId());
}),
];
@@ -11,8 +11,8 @@ import { type Room } from "matrix-js-sdk/src/matrix";
import { InfoSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import ConfirmUserActionDialog from "./ConfirmUserActionDialog";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker";
import { SDKContextClass } from "../../../contexts/SDKContextClass.ts";
type BaseProps = ComponentProps<typeof ConfirmUserActionDialog>;
interface IProps extends Omit<BaseProps, "matrixClient" | "children" | "onFinished"> {
@@ -36,7 +36,7 @@ const ConfirmSpaceUserActionDialog: React.FC<IProps> = ({
...props
}) => {
const spaceChildren = useMemo(() => {
const children = SpaceStore.instance.getChildren(space.roomId);
const children = SDKContextClass.instance.spaceStore.getChildren(space.roomId);
if (spaceChildFilter) {
return children.filter(spaceChildFilter);
}
@@ -13,10 +13,10 @@ import { InfoSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icon
import { _t } from "../../../languageHandler";
import DialogButtons from "../elements/DialogButtons";
import BaseDialog from "../dialogs/BaseDialog";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker";
import { filterBoolean } from "../../../utils/arrays";
import { isOnlyAdmin } from "../../../utils/membership";
import { SDKContextClass } from "../../../contexts/SDKContextClass.ts";
interface IProps {
space: Room;
@@ -25,8 +25,8 @@ interface IProps {
const LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => {
const spaceChildren = useMemo(() => {
const roomSet = new Set(SpaceStore.instance.getSpaceFilteredRoomIds(space.roomId));
SpaceStore.instance.traverseSpace(
const roomSet = new Set(SDKContextClass.instance.spaceStore.getSpaceFilteredRoomIds(space.roomId));
SDKContextClass.instance.spaceStore.traverseSpace(
space.roomId,
(spaceId) => {
if (space.roomId === spaceId) return; // skip the root node
@@ -15,12 +15,12 @@ import { AutoHideScrollbar } from "@element-hq/web-shared-components";
import { _t } from "../../../languageHandler";
import BaseDialog from "./BaseDialog";
import SearchBox from "../../structures/SearchBox";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import RoomAvatar from "../avatars/RoomAvatar";
import AccessibleButton from "../elements/AccessibleButton";
import StyledCheckbox from "../elements/StyledCheckbox";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { filterBoolean } from "../../../utils/arrays";
import { SDKContextClass } from "../../../contexts/SDKContextClass.ts";
interface IProps {
room: Room;
@@ -38,7 +38,7 @@ const Entry: React.FC<{
let description;
if (localRoom) {
description = _t("common|n_members", { count: room.getJoinedMemberCount() });
const numChildRooms = SpaceStore.instance.getChildRooms(room.roomId).length;
const numChildRooms = SDKContextClass.instance.spaceStore.getChildRooms(room.roomId).length;
if (numChildRooms > 0) {
description += " · " + _t("common|n_rooms", { count: numChildRooms });
}
@@ -67,7 +67,7 @@ const Entry: React.FC<{
const addAllParents = (set: Set<Room>, room: Room): void => {
const cli = room.client;
const parents = Array.from(SpaceStore.instance.getKnownParents(room.roomId)).map((parentId) =>
const parents = Array.from(SDKContextClass.instance.spaceStore.getKnownParents(room.roomId)).map((parentId) =>
cli.getRoom(parentId),
);
@@ -90,7 +90,7 @@ const ManageRestrictedJoinRuleDialog: React.FC<IProps> = ({ room, selected, onFi
return [
Array.from(parents),
SpaceStore.instance.spacePanelSpaces.filter((s) => !parents.has(s)),
SDKContextClass.instance.spaceStore.spacePanelSpaces.filter((s) => !parents.has(s)),
filterBoolean(
selected?.map((roomId) => {
const room = cli.getRoom(roomId);
@@ -72,7 +72,6 @@ import { RoomNotificationStateStore } from "../../../../stores/notifications/Roo
import { compareRoomsByRecency } from "../../../../utils/room/sortRoomsByRecency";
import { SDKContextClass } from "../../../../contexts/SDKContextClass";
import { getMetaSpaceName, MetaSpace } from "../../../../stores/spaces";
import SpaceStore from "../../../../stores/spaces/SpaceStore";
import { DirectoryMember, type Member, startDmOnFirstMessage } from "../../../../utils/direct-messages";
import DMRoomMap from "../../../../utils/DMRoomMap";
import { makeUserPermalink } from "../../../../utils/permalinks/Permalinks";
@@ -420,13 +419,13 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
}
return [
...SpaceStore.instance.enabledMetaSpaces.map((spaceKey) => ({
...SDKContextClass.instance.spaceStore.enabledMetaSpaces.map((spaceKey) => ({
section: Section.Spaces,
filter: [] as Filter[],
avatar: <div className="mx_SpotlightDialog_metaspaceResult">{metaspaceToIcon(spaceKey)}</div>,
name: getMetaSpaceName(spaceKey, SpaceStore.instance.allRoomsInHome),
name: getMetaSpaceName(spaceKey, SDKContextClass.instance.spaceStore.allRoomsInHome),
onClick() {
SpaceStore.instance.setActiveSpace(spaceKey);
SDKContextClass.instance.spaceStore.setActiveSpace(spaceKey);
},
})),
...roomResults,
@@ -517,7 +516,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
const numResults = sum(Object.values(results).map((it) => it.length));
useWebSearchMetrics(numResults, query.length, true);
const activeSpace = SpaceStore.instance.activeSpaceRoom;
const activeSpace = SDKContextClass.instance.spaceStore.activeSpaceRoom;
const [spaceResults, spaceResultsLoading] = useSpaceResults(activeSpace ?? undefined, query);
const setQuery = (e: ChangeEvent<HTMLInputElement>): void => {
@@ -12,7 +12,6 @@ import { KnownMembership } from "matrix-js-sdk/src/types";
import { ErrorSolidIcon, UserAddIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { EventTileBubble, LinkedText } from "@element-hq/web-shared-components";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import DMRoomMap from "../../../utils/DMRoomMap";
import { _t, _td } from "../../../languageHandler";
import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton";
@@ -21,10 +20,8 @@ import RoomAvatar from "../avatars/RoomAvatar";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { type ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
import { Action } from "../../../dispatcher/actions";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { showSpaceInvite } from "../../../utils/space";
import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog-tab";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
import { privateShouldBeEncrypted } from "../../../utils/rooms";
@@ -33,6 +30,7 @@ import { shouldEncryptRoomWithSingle3rdPartyInvite } from "../../../utils/room/s
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
import { useTopic } from "../../../hooks/room/useTopic";
import { topicToHtml } from "../../../HtmlUtils";
import { SDKContext } from "../../../contexts/SDKContext.ts";
function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean {
const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId);
@@ -53,7 +51,8 @@ const determineIntroMessage = (room: Room, encryptedSingle3rdPartyInvite: boolea
};
const NewRoomIntro: React.FC = () => {
const cli = useContext(MatrixClientContext);
const sdkContext = useContext(SDKContext);
const cli = sdkContext.client!;
const { room, roomId } = useScopedRoomContext("room", "roomId");
const topic = useTopic(room);
const isLocalRoom = room instanceof LocalRoom;
@@ -179,10 +178,10 @@ const NewRoomIntro: React.FC = () => {
let parentSpace: Room | undefined;
if (
SpaceStore.instance.activeSpaceRoom?.canInvite(cli.getSafeUserId()) &&
SpaceStore.instance.isRoomInSpace(SpaceStore.instance.activeSpace!, room.roomId)
sdkContext.spaceStore.activeSpaceRoom?.canInvite(cli.getSafeUserId()) &&
sdkContext.spaceStore.isRoomInSpace(sdkContext.spaceStore.activeSpace!, room.roomId)
) {
parentSpace = SpaceStore.instance.activeSpaceRoom;
parentSpace = sdkContext.spaceStore.activeSpaceRoom;
}
let buttons: JSX.Element | undefined;
@@ -278,10 +277,7 @@ const NewRoomIntro: React.FC = () => {
const subText = _t("room|intro|private_unencrypted_warning");
let subButton: JSX.Element | undefined;
if (
room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, MatrixClientPeg.safeGet()) &&
!isLocalRoom
) {
if (room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, cli) && !isLocalRoom) {
subButton = (
<AccessibleButton kind="link_inline" onClick={openRoomSettings}>
{_t("room|intro|enable_encryption_prompt")}
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { useState, useCallback } from "react";
import React, { useState, useCallback, useContext } from "react";
import { Flex, RoomListHeaderView, useCreateAutoDisposedViewModel } from "@element-hq/web-shared-components";
import { shouldShowComponent } from "../../../../customisations/helpers/UIComponents";
@@ -19,7 +19,7 @@ import { Landmark, LandmarkNavigation } from "../../../../accessibility/Landmark
import { type IState as IRovingTabIndexState } from "../../../../accessibility/RovingTabIndex";
import { RoomListHeaderViewModel } from "../../../../viewmodels/room-list/RoomListHeaderViewModel";
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
import SpaceStore from "../../../../stores/spaces/SpaceStore";
import { SDKContext } from "../../../../contexts/SDKContext.ts";
type RoomListPanelProps = {
/**
@@ -33,6 +33,7 @@ type RoomListPanelProps = {
* The panel of the room list
*/
export const RoomListPanel: React.FC<RoomListPanelProps> = ({ activeSpace }) => {
const sdkContext = useContext(SDKContext);
const displayRoomSearch = shouldShowComponent(UIComponent.FilterContainer);
const [focusedElement, setFocusedElement] = useState<Element | null>(null);
@@ -62,7 +63,7 @@ export const RoomListPanel: React.FC<RoomListPanelProps> = ({ activeSpace }) =>
const matrixClient = useMatrixClientContext();
const vm = useCreateAutoDisposedViewModel(
() => new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }),
() => new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }),
);
return (
@@ -14,7 +14,6 @@ import StyledRadioGroup, { type IDefinition } from "../elements/StyledRadioGroup
import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import RoomAvatar from "../avatars/RoomAvatar";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import Modal from "../../../Modal";
import ManageRestrictedJoinRuleDialog from "../dialogs/ManageRestrictedJoinRuleDialog";
import RoomUpgradeWarningDialog, { type IFinishedOpts } from "../dialogs/RoomUpgradeWarningDialog";
@@ -28,6 +27,7 @@ import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPaylo
import { doesRoomVersionSupport, PreferredRoomVersions } from "../../../utils/PreferredRoomVersions";
import SettingsStore from "../../../settings/SettingsStore";
import LabelledCheckbox from "../elements/LabelledCheckbox";
import { SDKContextClass } from "../../../contexts/SDKContextClass.ts";
export interface JoinRuleSettingsProps {
room: Room;
@@ -92,8 +92,8 @@ const JoinRuleSettings: React.FC<JoinRuleSettingsProps> = ({
const editRestrictedRoomIds = async (): Promise<string[] | undefined> => {
let selected = restrictedAllowRoomIds;
if (!selected?.length && SpaceStore.instance.activeSpaceRoom) {
selected = [SpaceStore.instance.activeSpaceRoom.roomId];
if (!selected?.length && SDKContextClass.instance.spaceStore.activeSpaceRoom) {
selected = [SDKContextClass.instance.spaceStore.activeSpaceRoom.roomId];
}
const { finished } = Modal.createDialog(
@@ -296,12 +296,12 @@ const JoinRuleSettings: React.FC<JoinRuleSettingsProps> = ({
</div>
</div>
);
} else if (SpaceStore.instance.activeSpaceRoom) {
} else if (SDKContextClass.instance.spaceStore.activeSpaceRoom) {
description = _t(
"room_settings|security|join_rule_restricted_description_active_space",
{},
{
spaceName: () => <strong>{SpaceStore.instance.activeSpaceRoom!.name}</strong>,
spaceName: () => <strong>{SDKContextClass.instance.spaceStore.activeSpaceRoom!.name}</strong>,
},
);
} else {
@@ -365,9 +365,9 @@ const JoinRuleSettings: React.FC<JoinRuleSettingsProps> = ({
let warning: JSX.Element | undefined;
const userId = cli.getUserId()!;
const unableToUpdateSomeParents = Array.from(SpaceStore.instance.getKnownParents(room.roomId)).some(
(roomId) => !cli.getRoom(roomId)?.currentState.maySendStateEvent(EventType.SpaceChild, userId),
);
const unableToUpdateSomeParents = Array.from(
SDKContextClass.instance.spaceStore.getKnownParents(room.roomId),
).some((roomId) => !cli.getRoom(roomId)?.currentState.maySendStateEvent(EventType.SpaceChild, userId));
if (unableToUpdateSomeParents) {
warning = <strong>{_t("room_settings|security|join_rule_restricted_upgrade_warning")}</strong>;
}
@@ -38,7 +38,6 @@ import { useContextMenu } from "../../structures/ContextMenu";
import SpaceCreateMenu from "./SpaceCreateMenu";
import { SpaceButton, SpaceItem } from "./SpaceTreeLevel";
import { useEventEmitter, useEventEmitterState } from "../../../hooks/useEventEmitter";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import {
getMetaSpaceName,
MetaSpace,
@@ -49,10 +48,7 @@ import {
UPDATE_TOP_LEVEL_SPACES,
} from "../../../stores/spaces";
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
import {
RoomNotificationStateStore,
UPDATE_STATUS_INDICATOR,
} from "../../../stores/notifications/RoomNotificationStateStore";
import { UPDATE_STATUS_INDICATOR } from "../../../stores/notifications/RoomNotificationStateStore";
import type SpaceContextMenu from "../context_menus/SpaceContextMenu";
import IconizedContextMenu, {
IconizedContextMenuCheckbox,
@@ -82,18 +78,20 @@ import { useModuleSpacePanelItems } from "../../../modules/ExtrasApi.ts";
import { UserMenuViewModel } from "../../../viewmodels/menus/UserMenuViewModel.ts";
import { SDKContext } from "../../../contexts/SDKContext.ts";
import { OwnProfileStore } from "../../../stores/OwnProfileStore.ts";
import { type SDKContextClass } from "../../../contexts/SDKContextClass.ts";
const useSpaces = (): [Room[], MetaSpace[], Room[], SpaceKey] => {
const invites = useEventEmitterState<Room[]>(SpaceStore.instance, UPDATE_INVITED_SPACES, () => {
return SpaceStore.instance.invitedSpaces;
const sdkContext = useContext(SDKContext);
const invites = useEventEmitterState<Room[]>(sdkContext.spaceStore, UPDATE_INVITED_SPACES, () => {
return sdkContext.spaceStore.invitedSpaces;
});
const [metaSpaces, actualSpaces] = useEventEmitterState<[MetaSpace[], Room[]]>(
SpaceStore.instance,
sdkContext.spaceStore,
UPDATE_TOP_LEVEL_SPACES,
() => [SpaceStore.instance.enabledMetaSpaces, SpaceStore.instance.spacePanelSpaces],
() => [sdkContext.spaceStore.enabledMetaSpaces, sdkContext.spaceStore.spacePanelSpaces],
);
const activeSpace = useEventEmitterState<SpaceKey>(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => {
return SpaceStore.instance.activeSpace;
const activeSpace = useEventEmitterState<SpaceKey>(sdkContext.spaceStore, UPDATE_SELECTED_SPACE, () => {
return sdkContext.spaceStore.activeSpace;
});
return [invites, metaSpaces, actualSpaces, activeSpace];
};
@@ -144,22 +142,23 @@ const MetaSpaceButton: React.FC<IMetaSpaceButtonProps> = ({ selected, isPanelCol
);
};
const getHomeNotificationState = (): NotificationState => {
return SpaceStore.instance.allRoomsInHome
? RoomNotificationStateStore.instance.globalState
: SpaceStore.instance.getNotificationState(MetaSpace.Home);
const getHomeNotificationState = (sdkContext: SDKContextClass): NotificationState => {
return sdkContext.spaceStore.allRoomsInHome
? sdkContext.roomNotificationStateStore.globalState
: sdkContext.spaceStore.getNotificationState(MetaSpace.Home);
};
const HomeButton: React.FC<MetaSpaceButtonProps> = ({ selected, isPanelCollapsed }) => {
const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => {
return SpaceStore.instance.allRoomsInHome;
const sdkContext = useContext(SDKContext);
const allRoomsInHome = useEventEmitterState(sdkContext.spaceStore, UPDATE_HOME_BEHAVIOUR, () => {
return sdkContext.spaceStore.allRoomsInHome;
});
const [notificationState, setNotificationState] = useState(getHomeNotificationState());
const [notificationState, setNotificationState] = useState(getHomeNotificationState(sdkContext));
const updateNotificationState = useCallback(() => {
setNotificationState(getHomeNotificationState());
}, []);
setNotificationState(getHomeNotificationState(sdkContext));
}, [sdkContext]);
useEffect(updateNotificationState, [updateNotificationState, allRoomsInHome]);
useEventEmitter(RoomNotificationStateStore.instance, UPDATE_STATUS_INDICATOR, updateNotificationState);
useEventEmitter(sdkContext.roomNotificationStateStore, UPDATE_STATUS_INDICATOR, updateNotificationState);
return (
<MetaSpaceButton
@@ -177,13 +176,14 @@ const HomeButton: React.FC<MetaSpaceButtonProps> = ({ selected, isPanelCollapsed
};
const OrphansButton: React.FC<MetaSpaceButtonProps> = ({ selected, isPanelCollapsed }) => {
const sdkContext = useContext(SDKContext);
return (
<MetaSpaceButton
spaceKey={MetaSpace.Orphans}
selected={selected}
isPanelCollapsed={isPanelCollapsed}
label={getMetaSpaceName(MetaSpace.Orphans)}
notificationState={SpaceStore.instance.getNotificationState(MetaSpace.Orphans)}
notificationState={sdkContext.spaceStore.getNotificationState(MetaSpace.Orphans)}
size="32px"
icon={<RoomIcon />}
/>
@@ -191,13 +191,14 @@ const OrphansButton: React.FC<MetaSpaceButtonProps> = ({ selected, isPanelCollap
};
const VideoRoomsButton: React.FC<MetaSpaceButtonProps> = ({ selected, isPanelCollapsed }) => {
const sdkContext = useContext(SDKContext);
return (
<MetaSpaceButton
spaceKey={MetaSpace.VideoRooms}
selected={selected}
isPanelCollapsed={isPanelCollapsed}
label={getMetaSpaceName(MetaSpace.VideoRooms)}
notificationState={SpaceStore.instance.getNotificationState(MetaSpace.VideoRooms)}
notificationState={sdkContext.spaceStore.getNotificationState(MetaSpace.VideoRooms)}
size="32px"
icon={<VideoCallSolidIcon />}
/>
@@ -271,6 +272,7 @@ interface IInnerSpacePanelProps extends DroppableProvidedProps {
// Optimisation based on https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/droppable.md#recommended-droppable--performance-optimisation
const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(
({ children, isPanelCollapsed, setPanelCollapsed, isDraggingOver, innerRef, ...props }) => {
const sdkContext = useContext(SDKContext);
const [invites, metaSpaces, actualSpaces, activeSpace] = useSpaces();
const activeSpaces = activeSpace ? [activeSpace] : [];
@@ -342,7 +344,7 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(
size="32px"
selected={activeSpace === item.spaceKey}
onClick={() => {
SpaceStore.instance.setActiveSpace(item.spaceKey);
sdkContext.spaceStore.setActiveSpace(item.spaceKey);
item.onSelected?.();
}}
/>
@@ -403,7 +405,7 @@ const SpacePanel: React.FC = () => {
onDragEnd={(result) => {
setDragging(false);
if (!result.destination) return; // dropped outside the list
SpaceStore.instance.moveRootSpace(result.source.index, result.destination.index);
sdkContext.spaceStore.moveRootSpace(result.source.index, result.destination.index);
onDragEndHandler();
}}
>
@@ -15,6 +15,7 @@ import React, {
type InputHTMLAttributes,
type LegacyRef,
type RefObject,
useContext,
} from "react";
import classNames from "classnames";
import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix";
@@ -27,7 +28,6 @@ import {
} from "@vector-im/compound-design-tokens/assets/web/icons";
import RoomAvatar from "../avatars/RoomAvatar";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { type SpaceKey } from "../../../stores/spaces";
import SpaceTreeLevelLayoutStore from "../../../stores/spaces/SpaceTreeLevelLayoutStore";
import NotificationBadge from "../rooms/NotificationBadge";
@@ -47,6 +47,7 @@ import { type NotificationState } from "../../../stores/notifications/Notificati
import SpaceContextMenu from "../context_menus/SpaceContextMenu";
import { useRovingTabIndex } from "../../../accessibility/RovingTabIndex";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { SDKContext } from "../../../contexts/SDKContext.ts";
type ButtonProps<T extends keyof HTMLElementTagNameMap> = Omit<
AccessibleButtonProps<T>,
@@ -83,6 +84,7 @@ export const SpaceButton = <T extends keyof HTMLElementTagNameMap>({
ContextMenuComponent,
...props
}: ButtonProps<T>): JSX.Element => {
const sdkContext = useContext(SDKContext);
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>(innerRef);
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLDivElement>(handle);
const tabIndex = isActive ? 0 : -1;
@@ -108,7 +110,7 @@ export const SpaceButton = <T extends keyof HTMLElementTagNameMap>({
const jumpToNotification = (ev: MouseEvent): void => {
ev.stopPropagation();
ev.preventDefault();
SpaceStore.instance.setActiveRoomInSpace(spaceKey);
sdkContext.spaceStore.setActiveRoomInSpace(spaceKey);
};
notifBadge = (
@@ -139,7 +141,7 @@ export const SpaceButton = <T extends keyof HTMLElementTagNameMap>({
// space is set here because of the assignment condition of onClick
defaultDispatcher.dispatch({ action: Action.ViewRoom, room_id: space!.roomId });
const activateSpace = (): void => {
if (spaceKey) SpaceStore.instance.setActiveSpace(spaceKey);
if (spaceKey) sdkContext.spaceStore.setActiveSpace(spaceKey);
};
const onClick = props.onClick ?? (selected && space ? viewSpaceHome : activateSpace);
@@ -203,10 +205,13 @@ interface IItemState {
}
export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
public static contextType = SDKContext;
declare public context: React.ContextType<typeof SDKContext>;
private buttonRef = createRef<HTMLDivElement>();
public constructor(props: IItemProps) {
super(props);
public constructor(props: IItemProps, context: React.ContextType<typeof SDKContext>) {
super(props, context);
const collapsed = SpaceTreeLevelLayoutStore.instance.getSpaceCollapsedState(
props.space.roomId,
@@ -222,12 +227,12 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
}
public componentDidMount(): void {
SpaceStore.instance.on(this.props.space.roomId, this.onSpaceUpdate);
this.context.spaceStore.on(this.props.space.roomId, this.onSpaceUpdate);
this.props.space.on(RoomEvent.Name, this.onRoomNameChange);
}
public componentWillUnmount(): void {
SpaceStore.instance.off(this.props.space.roomId, this.onSpaceUpdate);
this.context.spaceStore.off(this.props.space.roomId, this.onSpaceUpdate);
this.props.space.off(RoomEvent.Name, this.onRoomNameChange);
}
@@ -244,7 +249,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
};
private get childSpaces(): Room[] {
return SpaceStore.instance
return this.context.spaceStore
.getChildSpaces(this.props.space.roomId)
.filter((s) => !this.props.parents?.has(s.roomId));
}
@@ -334,7 +339,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
const notificationState = isInvite
? StaticNotificationState.forSymbol("!", NotificationLevel.Highlight)
: SpaceStore.instance.getNotificationState(space.roomId);
: this.context.spaceStore.getNotificationState(space.roomId);
const hasChildren = this.state.childSpaces?.length;
+5 -4
View File
@@ -17,7 +17,7 @@ import { MemberListStore } from "../stores/MemberListStore";
import { RoomNotificationStateStore } from "../stores/notifications/RoomNotificationStateStore";
import RightPanelStore from "../stores/right-panel/RightPanelStore";
import { RoomViewStore } from "../stores/RoomViewStore";
import SpaceStore, { type SpaceStoreClass } from "../stores/spaces/SpaceStore";
import SpaceStore from "../stores/spaces/SpaceStore";
import TypingStore from "../stores/TypingStore";
import { UserProfilesStore } from "../stores/UserProfilesStore";
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
@@ -65,7 +65,7 @@ export class SDKContextClass {
protected _WidgetStore?: WidgetStore;
protected _PosthogAnalytics?: PosthogAnalytics;
protected _SlidingSyncManager?: SlidingSyncManager;
protected _SpaceStore?: SpaceStoreClass;
protected _SpaceStore?: SpaceStore;
protected _LegacyCallHandler?: LegacyCallHandler;
protected _TypingStore?: TypingStore;
protected _UserProfilesStore?: UserProfilesStore;
@@ -153,9 +153,10 @@ export class SDKContextClass {
}
return this._SlidingSyncManager;
}
public get spaceStore(): SpaceStoreClass {
public get spaceStore(): SpaceStore {
if (!this._SpaceStore) {
this._SpaceStore = SpaceStore.instance;
this._SpaceStore = new SpaceStore(defaultDispatcher, this);
this._SpaceStore.start();
}
return this._SpaceStore;
}
+2 -2
View File
@@ -33,7 +33,6 @@ import { _t, UserFriendlyError } from "./languageHandler";
import dis from "./dispatcher/dispatcher";
import * as Rooms from "./Rooms";
import { getAddressType } from "./UserAddress";
import SpaceStore from "./stores/spaces/SpaceStore";
import { makeSpaceParentEvent } from "./utils/space";
import { JitsiCall, ElementCall } from "./models/Call";
import { Action } from "./dispatcher/actions";
@@ -48,6 +47,7 @@ import { doesRoomVersionSupport, PreferredRoomVersions } from "./utils/Preferred
import { MEGOLM_ENCRYPTION_ALGORITHM } from "./utils/crypto";
import { ElementCallMemberEventType } from "./call-types";
import { htmlSerializeFromMdIfNeeded } from "./editor/serialize";
import { SDKContextClass } from "./contexts/SDKContextClass.ts";
import SdkConfig from "./SdkConfig";
// we define a number of interfaces which take their names from the js-sdk
@@ -384,7 +384,7 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
})
.then(() => {
if (opts.parentSpace) {
return SpaceStore.instance.addRoomToSpace(
return SDKContextClass.instance.spaceStore.addRoomToSpace(
opts.parentSpace,
roomId,
[client.getDomain()!],
+1 -1
View File
@@ -45,7 +45,7 @@ export abstract class AsyncStore<T extends object> extends EventEmitter {
* @param {T} initialState The initial state for the store.
*/
protected constructor(
private dispatcher: MatrixDispatcher,
protected dispatcher: MatrixDispatcher,
initialState: T = <T>{},
) {
super();
@@ -19,7 +19,6 @@ import { RecencySorter } from "./skip-list/sorters/RecencySorter";
import { AlphabeticSorter } from "./skip-list/sorters/AlphabeticSorter";
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
import { EffectiveMembership, getEffectiveMembership, getEffectiveMembershipTag } from "../../utils/membership";
import SpaceStore from "../spaces/SpaceStore";
import { type SpaceKey, UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../spaces";
import { FavouriteFilter } from "./skip-list/filters/FavouriteFilter";
import { UnreadFilter } from "./skip-list/filters/UnreadFilter";
@@ -49,6 +48,7 @@ import {
reorderSection,
} from "./section";
import { DefaultTagID, type TagID } from "./skip-list/tag";
import { SDKContextClass } from "../../contexts/SDKContextClass.ts";
/**
* These are the filters passed to the room skip list.
@@ -131,10 +131,10 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
public constructor(dispatcher: MatrixDispatcher) {
super(dispatcher);
this.msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors");
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, () => {
SDKContextClass.instance.spaceStore.on(UPDATE_SELECTED_SPACE, () => {
this.onActiveSpaceChanged();
});
SpaceStore.instance.on(UPDATE_HOME_BEHAVIOUR, () => this.onActiveSpaceChanged());
SDKContextClass.instance.spaceStore.on(UPDATE_HOME_BEHAVIOUR, () => this.onActiveSpaceChanged());
SettingsStore.watchSetting("RoomList.OrderedCustomSections", null, () => this.onOrderedCustomSectionsChange());
this.loadCustomSections();
@@ -173,7 +173,7 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
* @param filterKeys Optional array of filters that the rooms must match against.
*/
public getSortedRoomsInActiveSpace(filterKeys?: FilterKey[]): RoomsResult {
const spaceId = SpaceStore.instance.activeSpace;
const spaceId = SDKContextClass.instance.spaceStore.activeSpace;
const areSectionsEnabled = SettingsStore.getValue("RoomList.showSections");
const sections = areSectionsEnabled
@@ -238,7 +238,7 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
this.roomSkipList = new RoomSkipList(sorter, this.getSkipListFilters());
await SpaceStore.instance.storeReadyPromise;
await SDKContextClass.instance.spaceStore.storeReadyPromise;
const rooms = this.getRooms();
this.roomSkipList.seed(rooms);
this.emit(LISTS_LOADED_EVENT);
@@ -522,7 +522,7 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
* Emits {@link SECTION_CREATED_EVENT} if the section was successfully created.
*/
public async createSection(): Promise<string | undefined> {
const tag = await createSection(SpaceStore.instance.activeSpace);
const tag = await createSection(SDKContextClass.instance.spaceStore.activeSpace);
if (!tag) return;
this.emit(SECTION_CREATED_EVENT, tag);
return tag;
+3 -3
View File
@@ -14,7 +14,7 @@ import { CreateSectionDialog } from "../../components/views/dialogs/CreateSectio
import { RemoveSectionDialog } from "../../components/views/dialogs/RemoveSectionDialog";
import { DefaultTagID, type TagID } from "./skip-list/tag";
import { isMetaSpace, MetaSpace, type SpaceKey } from "../spaces";
import SpaceStore from "../spaces/SpaceStore";
import { SDKContextClass } from "../../contexts/SDKContextClass.ts";
/**
* A synthetic tag used to represent the "Chats" section, which contains
@@ -105,8 +105,8 @@ function isReorderableSection(tag: string, customData: CustomSectionsData): tag
* Returns true if the given space key corresponds to an enabled meta-space or a known top-level space room.
*/
function doesSpaceExist(spaceId: SpaceKey): boolean {
if (isMetaSpace(spaceId)) return SpaceStore.instance.enabledMetaSpaces.includes(spaceId);
return SpaceStore.instance.spacePanelSpaces.some((room) => room.roomId === spaceId);
if (isMetaSpace(spaceId)) return SDKContextClass.instance.spaceStore.enabledMetaSpaces.includes(spaceId);
return SDKContextClass.instance.spaceStore.spacePanelSpaces.some((room) => room.roomId === spaceId);
}
/**
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
import type { Room } from "matrix-js-sdk/src/matrix";
import type { Filter, FilterKey } from "./filters";
import SpaceStore from "../../spaces/SpaceStore";
import { SDKContextClass } from "../../../contexts/SDKContextClass.ts";
/**
* Room skip list stores room nodes.
@@ -34,7 +34,7 @@ export class RoomNode {
/**
* Whether the room associated with this room node belongs to
* the currently active space.
* @see {@link SpaceStoreClass#activeSpace} to understand what active
* @see {@link SpaceStore#activeSpace} to understand what active
* space means.
*/
public get isInActiveSpace(): boolean {
@@ -46,8 +46,8 @@ export class RoomNode {
* in {@link RoomNode#isInActiveSpace}.
*/
public checkIfRoomBelongsToActiveSpace(): void {
const activeSpace = SpaceStore.instance.activeSpace;
this._isInActiveSpace = SpaceStore.instance.isRoomInSpace(activeSpace, this.room.roomId);
const activeSpace = SDKContextClass.instance.spaceStore.activeSpace;
this._isInActiveSpace = SDKContextClass.instance.spaceStore.isRoomInSpace(activeSpace, this.room.roomId);
}
/**
+45 -33
View File
@@ -23,7 +23,7 @@ import {
} from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import * as testUtils from "test-utils";
import { mkEvent, stubClient } from "test-utils";
import { mkEvent, setupAsyncStoreWithClient, stubClient } from "test-utils";
import SpaceStore from "./SpaceStore";
import {
@@ -44,6 +44,7 @@ import { DefaultTagID } from "../room-list-v3/skip-list/tag";
import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
import { NotificationLevel } from "../notifications/NotificationLevel";
import { storeRoomAliasInCache } from "../../RoomAliasCache.ts";
import { TestSDKContext } from "../../../test/unit-tests/TestSDKContext.ts";
vi.useFakeTimers();
@@ -100,8 +101,10 @@ DMRoomMap.sharedInstance = { getUserIdForRoomId, getDMRoomsForUserId };
describe("SpaceStore", () => {
stubClient();
const store = SpaceStore.instance;
const client = MatrixClientPeg.safeGet();
const sdkContext = new TestSDKContext();
sdkContext._client = client;
const store = sdkContext.spaceStore;
const spyDispatcher = vi.spyOn(defaultDispatcher, "dispatch");
@@ -1231,39 +1234,39 @@ describe("SpaceStore", () => {
rootSpace.getMyMembership.mockReturnValue(KnownMembership.Invite);
client.emit(ClientEvent.Room, rootSpace);
vi.runOnlyPendingTimers();
expect(SpaceStore.instance.invitedSpaces).toStrictEqual([rootSpace]);
expect(SpaceStore.instance.spacePanelSpaces).toStrictEqual([]);
expect(sdkContext.spaceStore.invitedSpaces).toStrictEqual([rootSpace]);
expect(sdkContext.spaceStore.spacePanelSpaces).toStrictEqual([]);
// accept invite to space
rootSpace.getMyMembership.mockReturnValue(KnownMembership.Join);
client.emit(RoomEvent.MyMembership, rootSpace, KnownMembership.Join, KnownMembership.Invite);
vi.runOnlyPendingTimers();
expect(SpaceStore.instance.invitedSpaces).toStrictEqual([]);
expect(SpaceStore.instance.spacePanelSpaces).toStrictEqual([rootSpace]);
expect(sdkContext.spaceStore.invitedSpaces).toStrictEqual([]);
expect(sdkContext.spaceStore.spacePanelSpaces).toStrictEqual([rootSpace]);
// join room in space
expect(SpaceStore.instance.isRoomInSpace(space1, room1)).toBeFalsy();
expect(sdkContext.spaceStore.isRoomInSpace(space1, room1)).toBeFalsy();
const rootSpaceRoom1 = mkRoom(room1);
rootSpaceRoom1.getMyMembership.mockReturnValue(KnownMembership.Join);
client.emit(ClientEvent.Room, rootSpaceRoom1);
vi.runOnlyPendingTimers();
expect(SpaceStore.instance.invitedSpaces).toStrictEqual([]);
expect(SpaceStore.instance.spacePanelSpaces).toStrictEqual([rootSpace]);
expect(SpaceStore.instance.isRoomInSpace(space1, room1)).toBeTruthy();
expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Home, room1)).toBeFalsy();
expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Orphans, room1)).toBeFalsy();
expect(sdkContext.spaceStore.invitedSpaces).toStrictEqual([]);
expect(sdkContext.spaceStore.spacePanelSpaces).toStrictEqual([rootSpace]);
expect(sdkContext.spaceStore.isRoomInSpace(space1, room1)).toBeTruthy();
expect(sdkContext.spaceStore.isRoomInSpace(MetaSpace.Home, room1)).toBeFalsy();
expect(sdkContext.spaceStore.isRoomInSpace(MetaSpace.Orphans, room1)).toBeFalsy();
// receive room invite
expect(SpaceStore.instance.isRoomInSpace(space1, room2)).toBeFalsy();
expect(sdkContext.spaceStore.isRoomInSpace(space1, room2)).toBeFalsy();
const rootSpaceRoom2 = mkRoom(room2);
rootSpaceRoom2.getMyMembership.mockReturnValue(KnownMembership.Invite);
client.emit(ClientEvent.Room, rootSpaceRoom2);
vi.runOnlyPendingTimers();
expect(SpaceStore.instance.invitedSpaces).toStrictEqual([]);
expect(SpaceStore.instance.spacePanelSpaces).toStrictEqual([rootSpace]);
expect(SpaceStore.instance.isRoomInSpace(space1, room2)).toBeTruthy();
expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Home, room2)).toBeTruthy();
expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Orphans, room2)).toBeFalsy();
expect(sdkContext.spaceStore.invitedSpaces).toStrictEqual([]);
expect(sdkContext.spaceStore.spacePanelSpaces).toStrictEqual([rootSpace]);
expect(sdkContext.spaceStore.isRoomInSpace(space1, room2)).toBeTruthy();
expect(sdkContext.spaceStore.isRoomInSpace(MetaSpace.Home, room2)).toBeTruthy();
expect(sdkContext.spaceStore.isRoomInSpace(MetaSpace.Orphans, room2)).toBeFalsy();
// start DM in space
const myRootSpaceMember = new RoomMember(space1, testUserId);
@@ -1280,7 +1283,7 @@ describe("SpaceStore", () => {
}
return null;
});
expect(SpaceStore.instance.getSpaceFilteredUserIds(space1)!.has(dm1Partner.userId)).toBeFalsy();
expect(sdkContext.spaceStore.getSpaceFilteredUserIds(space1)!.has(dm1Partner.userId)).toBeFalsy();
const memberEvent = mkEvent({
event: true,
type: EventType.RoomMember,
@@ -1293,25 +1296,25 @@ describe("SpaceStore", () => {
});
client.emit(RoomStateEvent.Members, memberEvent, rootSpace.currentState, dm1Partner);
vi.runOnlyPendingTimers();
expect(SpaceStore.instance.getSpaceFilteredUserIds(space1)!.has(dm1Partner.userId)).toBeTruthy();
expect(sdkContext.spaceStore.getSpaceFilteredUserIds(space1)!.has(dm1Partner.userId)).toBeTruthy();
const dm1Room = mkRoom(dm1);
dm1Room.getMyMembership.mockReturnValue(KnownMembership.Join);
client.emit(ClientEvent.Room, dm1Room);
vi.runOnlyPendingTimers();
expect(SpaceStore.instance.invitedSpaces).toStrictEqual([]);
expect(SpaceStore.instance.spacePanelSpaces).toStrictEqual([rootSpace]);
expect(SpaceStore.instance.isRoomInSpace(space1, dm1)).toBeTruthy();
expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Home, dm1)).toBeTruthy();
expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Orphans, dm1)).toBeFalsy();
expect(sdkContext.spaceStore.invitedSpaces).toStrictEqual([]);
expect(sdkContext.spaceStore.spacePanelSpaces).toStrictEqual([rootSpace]);
expect(sdkContext.spaceStore.isRoomInSpace(space1, dm1)).toBeTruthy();
expect(sdkContext.spaceStore.isRoomInSpace(MetaSpace.Home, dm1)).toBeTruthy();
expect(sdkContext.spaceStore.isRoomInSpace(MetaSpace.Orphans, dm1)).toBeFalsy();
// join subspace
const subspace = mkSpace(space2);
subspace.getMyMembership.mockReturnValue(KnownMembership.Join);
const prom = testUtils.emitPromise(SpaceStore.instance, space1);
const prom = testUtils.emitPromise(sdkContext.spaceStore, space1);
client.emit(ClientEvent.Room, subspace);
vi.runOnlyPendingTimers();
expect(SpaceStore.instance.invitedSpaces).toStrictEqual([]);
expect(SpaceStore.instance.spacePanelSpaces.map((r) => r.roomId)).toStrictEqual([rootSpace.roomId]);
expect(sdkContext.spaceStore.invitedSpaces).toStrictEqual([]);
expect(sdkContext.spaceStore.spacePanelSpaces.map((r) => r.roomId)).toStrictEqual([rootSpace.roomId]);
await prom;
});
@@ -1340,7 +1343,7 @@ describe("SpaceStore", () => {
it("Favourites and People meta spaces should not be returned", async () => {
await run();
// Favourites and People meta spaces are not part of the meta space order
expect(SpaceStore.instance.enabledMetaSpaces).toStrictEqual([MetaSpace.Home, MetaSpace.Orphans]);
expect(sdkContext.spaceStore.enabledMetaSpaces).toStrictEqual([MetaSpace.Home, MetaSpace.Orphans]);
});
describe("when feature_dynamic_room_predecessors is not enabled", () => {
@@ -1349,7 +1352,7 @@ describe("SpaceStore", () => {
(settingName) => settingName === "Spaces.allRoomsInHome",
);
// @ts-ignore calling a private function
SpaceStore.instance.onAction({
sdkContext.spaceStore.onAction({
action: Action.SettingUpdated,
settingName: "feature_dynamic_room_predecessors",
roomId: null,
@@ -1379,8 +1382,12 @@ describe("SpaceStore", () => {
});
it("passes that value in calls to getVisibleRooms during getSpaceFilteredRoomIds", () => {
const sdkContext = new TestSDKContext();
sdkContext._client = client;
// Given a store
const store = SpaceStore.testInstance();
const store = new SpaceStore(defaultDispatcher, sdkContext);
sdkContext._SpaceStore = store;
setupAsyncStoreWithClient(store, client);
// When we ask for filtered room ids
store.getSpaceFilteredRoomIds(MetaSpace.Home);
@@ -1399,7 +1406,7 @@ describe("SpaceStore", () => {
settingName === "Spaces.allRoomsInHome" || settingName === "feature_dynamic_room_predecessors",
);
// @ts-ignore calling a private function
SpaceStore.instance.onAction({
sdkContext.spaceStore.onAction({
action: Action.SettingUpdated,
settingName: "feature_dynamic_room_predecessors",
roomId: null,
@@ -1429,8 +1436,13 @@ describe("SpaceStore", () => {
});
it("passes that value in calls to getVisibleRooms during getSpaceFilteredRoomIds", () => {
const sdkContext = new TestSDKContext();
sdkContext._client = client;
// Given a store
const store = SpaceStore.testInstance();
const store = new SpaceStore(defaultDispatcher, sdkContext);
sdkContext._SpaceStore = store;
setupAsyncStoreWithClient(store, client);
// When we ask for filtered room ids
store.getSpaceFilteredRoomIds(MetaSpace.Home);
+17 -35
View File
@@ -23,7 +23,7 @@ import { KnownMembership } from "matrix-js-sdk/src/types";
import { logger } from "matrix-js-sdk/src/logger";
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { type MatrixDispatcher } from "../../dispatcher/dispatcher.ts";
import RoomListStoreV3 from "../room-list-v3/RoomListStoreV3";
import SettingsStore from "../../settings/SettingsStore";
import DMRoomMap from "../../utils/DMRoomMap";
@@ -59,7 +59,7 @@ import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"
import { type ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload";
import { type SwitchSpacePayload } from "../../dispatcher/payloads/SwitchSpacePayload";
import { type AfterLeaveRoomPayload } from "../../dispatcher/payloads/AfterLeaveRoomPayload";
import { SDKContextClass } from "../../contexts/SDKContextClass";
import { type SDKContextClass } from "../../contexts/SDKContextClass";
import { ModuleApi } from "../../modules/Api.ts";
const ACTIVE_SPACE_LS_KEY = "mx_active_space";
@@ -110,7 +110,7 @@ type SpaceStoreActions =
| SwitchSpacePayload
| AfterLeaveRoomPayload;
export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
export default class SpaceStore extends AsyncStoreWithClient<EmptyObject> {
// The spaces representing the roots of the various tree-like hierarchies
private rootSpaces: Room[] = [];
// Map from room/space ID to set of spaces which list it as a child
@@ -142,8 +142,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
private _msc3946ProcessDynamicPredecessor: boolean = SettingsStore.getValue("feature_dynamic_room_predecessors");
private _storeReadyDeferred = Promise.withResolvers<void>();
public constructor() {
super(defaultDispatcher, {});
public constructor(
dispatcher: MatrixDispatcher,
private readonly sdkContext: SDKContextClass,
) {
super(dispatcher, {});
SettingsStore.monitorSetting("Spaces.allRoomsInHome", null);
SettingsStore.monitorSetting("Spaces.enabledMetaSpaces", null);
@@ -208,7 +211,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
}
if (!!roomId) {
defaultDispatcher.dispatch<ViewRoomPayload>({
this.dispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: roomId,
context_switch: true,
@@ -252,14 +255,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
this.matrixClient.getRoom(roomId)?.getMyMembership() === KnownMembership.Join &&
this.isRoomInSpace(space, roomId)
) {
defaultDispatcher.dispatch<ViewRoomPayload>({
this.dispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: roomId,
context_switch: true,
metricsTrigger: "WebSpaceContextSwitch",
});
} else if (cliSpace) {
defaultDispatcher.dispatch<ViewRoomPayload>({
this.dispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: space,
context_switch: true,
@@ -268,7 +271,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
} else if (ModuleApi.instance.extras.spacePanelItems.has(space)) {
// module will handle this
} else {
defaultDispatcher.dispatch<ViewHomePagePayload>({
this.dispatcher.dispatch<ViewHomePagePayload>({
action: Action.ViewHomePage,
context_switch: true,
});
@@ -283,7 +286,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
// Load all members for the selected space and its subspaces,
// so we can correctly show DMs we have with members of this space.
SpaceStore.instance.traverseSpace(
this.traverseSpace(
space,
(roomId) => {
this.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded();
@@ -718,7 +721,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
// Method for resolving the impact of a single user's membership change in the given Space and its hierarchy
private onMemberUpdate = (space: Room, userId: string): void => {
const inSpace = SpaceStoreClass.isInSpace(space.getMember(userId));
const inSpace = SpaceStore.isInSpace(space.getMember(userId));
if (inSpace) {
this.userIdsBySpace.get(space.roomId)?.add(userId);
@@ -867,7 +870,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
this.updateNotificationStates([...changeSet]);
};
private switchSpaceIfNeeded = (roomId = SDKContextClass.instance.roomViewStore.getRoomId()): void => {
private switchSpaceIfNeeded = (roomId = this.sdkContext.roomViewStore.getRoomId()): void => {
if (!roomId) return;
if (!this.isRoomInSpace(this.activeSpace, roomId) && !this.matrixClient?.getRoom(roomId)?.isSpaceRoom()) {
this.switchToRelatedSpace(roomId);
@@ -923,7 +926,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
// if the room currently being viewed was just joined then switch to its related space
if (
newMembership === KnownMembership.Join &&
room.roomId === SDKContextClass.instance.roomViewStore.getRoomId()
room.roomId === this.sdkContext.roomViewStore.getRoomId()
) {
this.switchSpaceIfNeeded(room.roomId);
}
@@ -951,7 +954,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
this.emit(room.roomId);
}
if (membership === KnownMembership.Join && room.roomId === SDKContextClass.instance.roomViewStore.getRoomId()) {
if (membership === KnownMembership.Join && room.roomId === this.sdkContext.roomViewStore.getRoomId()) {
// if the user was looking at the space and then joined: select that space
this.setActiveSpace(room.roomId, false);
} else if (membership === KnownMembership.Leave && room.roomId === this.activeSpace) {
@@ -1349,24 +1352,3 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
this.notifyIfOrderChanged();
}
}
export default class SpaceStore {
private static readonly internalInstance = (() => {
const instance = new SpaceStoreClass();
instance.start();
return instance;
})();
public static get instance(): SpaceStoreClass {
return SpaceStore.internalInstance;
}
/**
* @internal for test only
*/
public static testInstance(): SpaceStoreClass {
const store = new SpaceStoreClass();
store.start();
return store;
}
}
+2 -2
View File
@@ -14,9 +14,9 @@ import { showAnyInviteErrors } from "../RoomInvite";
import Modal, { type IHandle } from "../Modal";
import { _t } from "../languageHandler";
import ErrorDialog from "../components/views/dialogs/ErrorDialog";
import SpaceStore from "../stores/spaces/SpaceStore";
import Spinner from "../components/views/elements/Spinner";
import MultiInviter, { type MultiInviterOptions } from "./MultiInviter";
import { SDKContextClass } from "../contexts/SDKContextClass.ts";
export interface RoomUpgradeProgress {
roomUpgraded: boolean;
@@ -73,7 +73,7 @@ export async function upgradeRoom(
let parentsToRelink: Room[] = [];
if (updateSpaces) {
parentsToRelink = Array.from(SpaceStore.instance.getKnownParents(room.roomId))
parentsToRelink = Array.from(SDKContextClass.instance.spaceStore.getKnownParents(room.roomId))
.map((roomId) => cli.getRoom(roomId))
.filter((parent) =>
parent?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()!),
+2 -2
View File
@@ -8,10 +8,10 @@ Please see LICENSE files in the repository root for full details.
import { type Room } from "matrix-js-sdk/src/matrix";
import SpaceStore from "../stores/spaces/SpaceStore";
import { _t } from "../languageHandler";
import DMRoomMap from "./DMRoomMap";
import { formatList } from "./FormattingUtils";
import { SDKContextClass } from "../contexts/SDKContextClass.ts";
export interface RoomContextDetails {
details: string | null;
@@ -26,7 +26,7 @@ export function roomContextDetails(room: Room): RoomContextDetails | null {
return { details: dmPartner };
}
const [parent, secondParent, ...otherParents] = SpaceStore.instance.getKnownParents(room.roomId);
const [parent, secondParent, ...otherParents] = SDKContextClass.instance.spaceStore.getKnownParents(room.roomId);
if (secondParent && !otherParents?.length) {
// exactly 2 edge case for improved i18n
const space1Name = room.client.getRoom(parent)?.name;
+8 -9
View File
@@ -20,7 +20,6 @@ import { Action } from "../dispatcher/actions";
import { leaveRoomBehaviour } from "./leave-behaviour";
import { SDKContextClass } from "../contexts/SDKContextClass";
import DMRoomMap from "../utils/DMRoomMap";
import SpaceStore from "../stores/spaces/SpaceStore";
import { MetaSpace } from "../stores/spaces";
import { type ActionPayload } from "../dispatcher/payloads";
import SettingsStore from "../settings/SettingsStore";
@@ -55,12 +54,12 @@ describe("leaveRoomBehaviour", () => {
}
});
await setupAsyncStoreWithClient(SpaceStore.instance, client);
await setupAsyncStoreWithClient(SDKContextClass.instance.spaceStore, client);
});
afterEach(async () => {
SpaceStore.instance.setActiveSpace(MetaSpace.Home);
await resetAsyncStoreWithClient(SpaceStore.instance);
SDKContextClass.instance.spaceStore.setActiveSpace(MetaSpace.Home);
await resetAsyncStoreWithClient(SDKContextClass.instance.spaceStore);
vi.restoreAllMocks();
});
@@ -114,11 +113,11 @@ describe("leaveRoomBehaviour", () => {
});
it("returns to the parent space after leaving a room inside of a space that was being viewed", async () => {
vi.spyOn(SpaceStore.instance, "getCanonicalParent").mockImplementation((roomId) =>
vi.spyOn(SDKContextClass.instance.spaceStore, "getCanonicalParent").mockImplementation((roomId) =>
roomId === room.roomId ? space : null,
);
viewRoom(room);
SpaceStore.instance.setActiveSpace(space.roomId, false);
SDKContextClass.instance.spaceStore.setActiveSpace(space.roomId, false);
await leaveRoomBehaviour(client, room.roomId);
await expectDispatch({
@@ -130,7 +129,7 @@ describe("leaveRoomBehaviour", () => {
it("returns to the home page after leaving a top-level space that was being viewed", async () => {
viewRoom(space);
SpaceStore.instance.setActiveSpace(space.roomId, false);
SDKContextClass.instance.spaceStore.setActiveSpace(space.roomId, false);
await leaveRoomBehaviour(client, space.roomId);
await expectDispatch({ action: Action.ViewHomePage });
@@ -138,11 +137,11 @@ describe("leaveRoomBehaviour", () => {
it("returns to the parent space after leaving a subspace that was being viewed", async () => {
room.isSpaceRoom.mockReturnValue(true);
vi.spyOn(SpaceStore.instance, "getCanonicalParent").mockImplementation((roomId) =>
vi.spyOn(SDKContextClass.instance.spaceStore, "getCanonicalParent").mockImplementation((roomId) =>
roomId === room.roomId ? space : null,
);
viewRoom(room);
SpaceStore.instance.setActiveSpace(room.roomId, false);
SDKContextClass.instance.spaceStore.setActiveSpace(room.roomId, false);
await leaveRoomBehaviour(client, room.roomId);
await expectDispatch({
+4 -5
View File
@@ -16,7 +16,6 @@ import Spinner from "../components/views/elements/Spinner";
import { _t } from "../languageHandler";
import ErrorDialog from "../components/views/dialogs/ErrorDialog";
import { isMetaSpace } from "../stores/spaces";
import SpaceStore from "../stores/spaces/SpaceStore";
import dis from "../dispatcher/dispatcher";
import { type ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../dispatcher/actions";
@@ -165,11 +164,11 @@ export async function leaveRoomBehaviour(
// accidentally viewing the next room in the list and clearing its
// notifications, switch to a neutral ground such as the home page or
// space landing page.
if (isMetaSpace(SpaceStore.instance.activeSpace)) {
if (isMetaSpace(SDKContextClass.instance.spaceStore.activeSpace)) {
dis.dispatch<ViewHomePagePayload>({ action: Action.ViewHomePage });
} else if (SpaceStore.instance.activeSpace === roomId) {
} else if (SDKContextClass.instance.spaceStore.activeSpace === roomId) {
// View the parent space, if there is one
const parent = SpaceStore.instance.getCanonicalParent(roomId);
const parent = SDKContextClass.instance.spaceStore.getCanonicalParent(roomId);
if (parent !== null) {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
@@ -182,7 +181,7 @@ export async function leaveRoomBehaviour(
} else {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: SpaceStore.instance.activeSpace,
room_id: SDKContextClass.instance.spaceStore.activeSpace,
metricsTrigger: undefined, // other
});
}
@@ -17,7 +17,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher";
import PosthogTrackers from "../../PosthogTrackers";
import { Action } from "../../dispatcher/actions";
import { getMetaSpaceName, type MetaSpace, UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import { type SpaceStoreClass } from "../../stores/spaces/SpaceStore";
import type SpaceStore from "../../stores/spaces/SpaceStore";
import {
shouldShowSpaceSettings,
showCreateNewRoom,
@@ -42,7 +42,7 @@ export interface Props {
/**
* The space store instance.
*/
spaceStore: SpaceStoreClass;
spaceStore: SpaceStore;
}
/**
@@ -271,7 +271,7 @@ export class RoomListHeaderViewModel
* @param matrixClient - The Matrix client instance.
* @returns
*/
function getInitialSnapshot(spaceStore: SpaceStoreClass, matrixClient: MatrixClient): RoomListHeaderViewSnapshot {
function getInitialSnapshot(spaceStore: SpaceStore, matrixClient: MatrixClient): RoomListHeaderViewSnapshot {
const sortingAlgorithm = SettingsStore.getValue("RoomList.preferredSorting");
let activeSortOption: SortOption;
@@ -300,7 +300,7 @@ function getInitialSnapshot(spaceStore: SpaceStoreClass, matrixClient: MatrixCli
* Get the header title based on the active space.
* @param spaceStore - The space store instance.
*/
function getHeaderTitle(spaceStore: SpaceStoreClass): string {
function getHeaderTitle(spaceStore: SpaceStore): string {
const activeSpace = spaceStore.activeSpaceRoom;
const spaceName = activeSpace?.name;
return spaceName ?? getMetaSpaceName(spaceStore.activeSpace as MetaSpace, spaceStore.allRoomsInHome);
@@ -321,7 +321,7 @@ function getCanCreateVideoRoom(canCreateRoom: boolean): boolean {
* @returns The header space state containing title, permissions, and display flags.
*/
function computeHeaderSpaceState(
spaceStore: SpaceStoreClass,
spaceStore: SpaceStore,
matrixClient: MatrixClient,
): Omit<RoomListHeaderViewSnapshot, "activeSortOption" | "isMessagePreviewEnabled"> {
const displaySectionReleaseAnnouncement =
@@ -22,7 +22,7 @@ import dispatcher from "../../dispatcher/dispatcher";
import { type ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { type RoomListSectionsCollapseStateChangedPayload } from "../../dispatcher/payloads/RoomListSectionsCollapseStateChangedPayload";
import { type SpaceStoreClass } from "../../stores/spaces/SpaceStore";
import type SpaceStore from "../../stores/spaces/SpaceStore";
import RoomListStoreV3, {
RoomListStoreV3Event,
type RoomsResult,
@@ -59,7 +59,7 @@ interface StickyRoomPosition {
interface RoomListViewModelProps {
client: MatrixClient;
roomViewStore: RoomViewStore;
spaceStore: SpaceStoreClass;
spaceStore: SpaceStore;
}
const filterKeyToIdMap: Map<FilterEnum, FilterId> = new Map([