Merge upstream v1.12.26 - reconnect the fork to Element Web (management #0099)
The repo had no upstream ancestry: a whole tree arrived in one commit in May, so
every update meant re-applying our patches by hand onto a fresh checkout, and a file
Element moved would take our lines with it silently.
The real base was found by measuring tree distance across develop rather than trusting
the changelog: deadd548, not the v1.12.17 tag. With that set as a temporary graft, this
merge computed as a proper three-way merge - 32 conflicts instead of 1757.
Resolutions, each decided rather than defaulted:
- 24 GitHub workflows stay deleted; we build on GitLab CI.
- MImageBody.tsx is gone upstream, migrated to MVVM. Our ClamAV error label moved into
ImageBodyViewModel.computeErrorLabel, ahead of the DecryptError branch, matching what
VideoBodyViewModel and FileBodyViewModel already do.
- Upstream extracted the room list item body into RoomListItemContent. Our call
participants list and its getInitials helper moved there; both sides' CSS classes and
both sides' props are kept.
- matrix-js-sdk follows upstream at 42.2.0 - our git ref pin was a workaround for a
stale ref, and following upstream is the point of this merge.
- Element Call stays ours. Checked before deciding: @element-hq/element-call-embedded
is referenced nowhere in the tree, while webpack.config.ts needs
@sorb/threadnet-call-embedded, so taking upstream's line would have deleted the noise
suppression from #0054 without a word.
The lockfile was regenerated with pnpm 11.20.0, which upstream now requires through
devEngines. CI already runs corepack enable, and onFail: download makes it fetch that
version by itself.
Not yet accepted: this needs a build and the ClamAV functional test - send an encrypted
file, receive a rejected one - before it goes near main.
This commit is contained in:
@@ -10,7 +10,6 @@ import { JoinRule, type MatrixClient, type Room, RoomEvent, RoomType } from "mat
|
||||
|
||||
import { RoomListHeaderViewModel } from "../../../src/viewmodels/room-list/RoomListHeaderViewModel";
|
||||
import { MetaSpace, UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../../../src/stores/spaces";
|
||||
import SpaceStore from "../../../src/stores/spaces/SpaceStore";
|
||||
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
@@ -26,9 +25,13 @@ import {
|
||||
import { createTestClient, mkSpace } from "../../test-utils";
|
||||
import { createRoom, hasCreateRoomRights } from "../../../src/viewmodels/room-list/utils";
|
||||
import PosthogTrackers from "../../../src/PosthogTrackers";
|
||||
import { ReleaseAnnouncementStore } from "../../../src/stores/ReleaseAnnouncementStore";
|
||||
import { TestSDKContext } from "../../unit-tests/TestSDKContext.ts";
|
||||
|
||||
jest.mock("../../../src/PosthogTrackers", () => ({
|
||||
trackInteraction: jest.fn(),
|
||||
trackSectionCreation: jest.fn(),
|
||||
trackCollapseOrExpandSection: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../src/utils/space", () => ({
|
||||
@@ -48,15 +51,21 @@ describe("RoomListHeaderViewModel", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
let mockSpace: Room;
|
||||
let vm: RoomListHeaderViewModel;
|
||||
let sdkContext: TestSDKContext;
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = createTestClient();
|
||||
sdkContext = new TestSDKContext();
|
||||
sdkContext._client = matrixClient;
|
||||
|
||||
mockSpace = mkSpace(matrixClient, "!space:server");
|
||||
|
||||
mocked(hasCreateRoomRights).mockReturnValue(true);
|
||||
mocked(shouldShowSpaceSettings).mockReturnValue(true);
|
||||
|
||||
jest.spyOn(ReleaseAnnouncementStore.instance, "getReleaseAnnouncement").mockReturnValue(null);
|
||||
jest.spyOn(ReleaseAnnouncementStore.instance, "nextReleaseAnnouncement").mockResolvedValue(undefined);
|
||||
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
|
||||
if (settingName === "RoomList.preferredSorting") return SortingAlgorithm.Recency;
|
||||
if (settingName === "feature_video_rooms") return true;
|
||||
@@ -73,14 +82,13 @@ describe("RoomListHeaderViewModel", () => {
|
||||
|
||||
describe("snapshot", () => {
|
||||
it("should compute snapshot for Home space", () => {
|
||||
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(MetaSpace.Home);
|
||||
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(null);
|
||||
jest.spyOn(sdkContext.spaceStore, "activeSpace", "get").mockReturnValue(MetaSpace.Home);
|
||||
jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(null);
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
|
||||
const snapshot = vm.getSnapshot();
|
||||
expect(snapshot.title).toBe("Home");
|
||||
expect(snapshot.displayComposeMenu).toBe(true);
|
||||
expect(snapshot.displaySpaceMenu).toBe(false);
|
||||
expect(snapshot.canCreateRoom).toBe(true);
|
||||
expect(snapshot.canCreateVideoRoom).toBe(true);
|
||||
@@ -88,10 +96,10 @@ describe("RoomListHeaderViewModel", () => {
|
||||
});
|
||||
|
||||
it("should compute snapshot for active space", () => {
|
||||
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(mockSpace.roomId);
|
||||
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(mockSpace);
|
||||
jest.spyOn(sdkContext.spaceStore, "activeSpace", "get").mockReturnValue(mockSpace.roomId);
|
||||
jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(mockSpace);
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
|
||||
const snapshot = vm.getSnapshot();
|
||||
expect(snapshot.title).toBe(mockSpace.roomId);
|
||||
@@ -103,7 +111,7 @@ describe("RoomListHeaderViewModel", () => {
|
||||
return false;
|
||||
});
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
expect(vm.getSnapshot().canCreateVideoRoom).toBe(false);
|
||||
});
|
||||
|
||||
@@ -113,41 +121,31 @@ describe("RoomListHeaderViewModel", () => {
|
||||
return false;
|
||||
});
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
expect(vm.getSnapshot().activeSortOption).toBe("alphabetical");
|
||||
});
|
||||
|
||||
it("should hide compose menu when user cannot create rooms", () => {
|
||||
mocked(hasCreateRoomRights).mockReturnValue(false);
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
|
||||
const snapshot = vm.getSnapshot();
|
||||
expect(snapshot.displayComposeMenu).toBe(false);
|
||||
expect(snapshot.canCreateRoom).toBe(false);
|
||||
});
|
||||
|
||||
it("should show invite option when space is public", () => {
|
||||
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(mockSpace.roomId);
|
||||
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(mockSpace);
|
||||
jest.spyOn(sdkContext.spaceStore, "activeSpace", "get").mockReturnValue(mockSpace.roomId);
|
||||
jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(mockSpace);
|
||||
jest.spyOn(mockSpace, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
expect(vm.getSnapshot().canInviteInSpace).toBe(true);
|
||||
});
|
||||
|
||||
it("should hide invite option when user cannot invite", () => {
|
||||
mocked(mockSpace.canInvite).mockReturnValue(false);
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
expect(vm.getSnapshot().canInviteInSpace).toBe(false);
|
||||
});
|
||||
|
||||
it("should hide space settings when user cannot access them", () => {
|
||||
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(mockSpace.roomId);
|
||||
jest.spyOn(sdkContext.spaceStore, "activeSpace", "get").mockReturnValue(mockSpace.roomId);
|
||||
mocked(shouldShowSpaceSettings).mockReturnValue(false);
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
expect(vm.getSnapshot().canAccessSpaceSettings).toBe(false);
|
||||
});
|
||||
|
||||
@@ -157,50 +155,72 @@ describe("RoomListHeaderViewModel", () => {
|
||||
return false;
|
||||
});
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
expect(vm.getSnapshot().isMessagePreviewEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[true, true, false],
|
||||
[false, false, true],
|
||||
])(
|
||||
"when feature_room_list_sections is %s: canCreateSection=%s, useComposeIcon=%s",
|
||||
(featureEnabled, expectedCanCreateSection, expectedUseComposeIcon) => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
|
||||
if (settingName === "feature_room_list_sections") return featureEnabled;
|
||||
return false;
|
||||
});
|
||||
it("should set areSectionsEnabled to true when RoomList.showSections is enabled", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
|
||||
if (settingName === "RoomList.showSections") return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
expect(vm.getSnapshot().canCreateSection).toBe(expectedCanCreateSection);
|
||||
expect(vm.getSnapshot().useComposeIcon).toBe(expectedUseComposeIcon);
|
||||
},
|
||||
);
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
expect(vm.getSnapshot().areSectionsEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it("should update areSectionsEnabled when RoomList.showSections setting changes", () => {
|
||||
let watchCallback: () => void = () => {};
|
||||
jest.spyOn(SettingsStore, "watchSetting").mockImplementation((settingName, _roomId, callback) => {
|
||||
if (settingName === "RoomList.showSections") watchCallback = callback as () => void;
|
||||
return "watcher-id";
|
||||
});
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
expect(vm.getSnapshot().areSectionsEnabled).toBe(false);
|
||||
|
||||
// Enable sections
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
|
||||
if (settingName === "RoomList.showSections") return true;
|
||||
return false;
|
||||
});
|
||||
watchCallback();
|
||||
|
||||
expect(vm.getSnapshot().areSectionsEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it("should set displaySectionReleaseAnnouncement to true when sections feature is enabled and announcement is active", () => {
|
||||
jest.spyOn(ReleaseAnnouncementStore.instance, "getReleaseAnnouncement").mockReturnValue(
|
||||
"room_list_section",
|
||||
);
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
expect(vm.getSnapshot().displaySectionReleaseAnnouncement).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("event listeners", () => {
|
||||
it.each([UPDATE_SELECTED_SPACE, UPDATE_HOME_BEHAVIOUR])(
|
||||
"should update snapshot when %s event is emitted",
|
||||
(event) => {
|
||||
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(MetaSpace.Home);
|
||||
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(null);
|
||||
jest.spyOn(sdkContext.spaceStore, "activeSpace", "get").mockReturnValue(MetaSpace.Home);
|
||||
jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(null);
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
|
||||
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(mockSpace.roomId);
|
||||
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(mockSpace);
|
||||
SpaceStore.instance.emit(event);
|
||||
jest.spyOn(sdkContext.spaceStore, "activeSpace", "get").mockReturnValue(mockSpace.roomId);
|
||||
jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(mockSpace);
|
||||
sdkContext.spaceStore.emit(event);
|
||||
|
||||
expect(vm.getSnapshot().title).toBe(mockSpace.roomId);
|
||||
},
|
||||
);
|
||||
|
||||
it("should update snapshot when space name changes", () => {
|
||||
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(mockSpace.roomId);
|
||||
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(mockSpace);
|
||||
jest.spyOn(sdkContext.spaceStore, "activeSpace", "get").mockReturnValue(mockSpace.roomId);
|
||||
jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(mockSpace);
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
|
||||
mockSpace.name = "new name";
|
||||
mockSpace.emit(RoomEvent.Name, mockSpace);
|
||||
@@ -211,20 +231,20 @@ describe("RoomListHeaderViewModel", () => {
|
||||
|
||||
describe("actions", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(mockSpace.roomId);
|
||||
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(mockSpace);
|
||||
jest.spyOn(sdkContext.spaceStore, "activeSpace", "get").mockReturnValue(mockSpace.roomId);
|
||||
jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(mockSpace);
|
||||
});
|
||||
|
||||
it("should fire CreateChat action when createChatRoom is called", () => {
|
||||
const fireSpy = jest.spyOn(defaultDispatcher, "fire");
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
|
||||
vm.createChatRoom(new Event("click"));
|
||||
expect(fireSpy).toHaveBeenCalledWith(Action.CreateChat);
|
||||
});
|
||||
|
||||
it("should call createRoom with active space when in a space", () => {
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
vm.createRoom(new Event("click"));
|
||||
|
||||
expect(createRoom).toHaveBeenCalledWith(mockSpace);
|
||||
@@ -236,16 +256,16 @@ describe("RoomListHeaderViewModel", () => {
|
||||
return false;
|
||||
});
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
vm.createVideoRoom();
|
||||
expect(showCreateNewRoom).toHaveBeenCalledWith(mockSpace, RoomType.ElementVideo);
|
||||
});
|
||||
|
||||
it("should use UnstableCall type when element_call_video_rooms is enabled", () => {
|
||||
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(null);
|
||||
jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(null);
|
||||
|
||||
const dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch");
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
vm.createVideoRoom();
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
@@ -256,7 +276,7 @@ describe("RoomListHeaderViewModel", () => {
|
||||
|
||||
it("should dispatch ViewRoom action when openSpaceHome is called", () => {
|
||||
const dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch");
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
vm.openSpaceHome();
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
@@ -267,21 +287,21 @@ describe("RoomListHeaderViewModel", () => {
|
||||
});
|
||||
|
||||
it("should show space invite dialog when inviteInSpace is called", () => {
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
vm.inviteInSpace();
|
||||
|
||||
expect(showSpaceInvite).toHaveBeenCalledWith(mockSpace);
|
||||
});
|
||||
|
||||
it("should show space preferences dialog when openSpacePreferences is called", () => {
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
vm.openSpacePreferences();
|
||||
|
||||
expect(showSpacePreferences).toHaveBeenCalledWith(mockSpace);
|
||||
});
|
||||
|
||||
it("should show space settings dialog when openSpaceSettings is called", () => {
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
vm.openSpaceSettings();
|
||||
|
||||
expect(showSpaceSettings).toHaveBeenCalledWith(mockSpace);
|
||||
@@ -293,7 +313,7 @@ describe("RoomListHeaderViewModel", () => {
|
||||
["unread-first" as const, SortingAlgorithm.Unread],
|
||||
])("should resort when sort is called with '%s'", (option, expectedAlgorithm) => {
|
||||
const resortSpy = jest.spyOn(RoomListStoreV3.instance, "resort").mockImplementation(jest.fn());
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
vm.sort(option);
|
||||
expect(resortSpy).toHaveBeenCalledWith(expectedAlgorithm);
|
||||
});
|
||||
@@ -304,7 +324,7 @@ describe("RoomListHeaderViewModel", () => {
|
||||
);
|
||||
PosthogTrackers.trackRoomListSortingAlgorithmChange = jest.fn();
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
jest.spyOn(RoomListStoreV3.instance, "resort").mockImplementation(jest.fn());
|
||||
vm.sort("unread-first");
|
||||
|
||||
@@ -318,7 +338,7 @@ describe("RoomListHeaderViewModel", () => {
|
||||
const createSectionSpy = jest
|
||||
.spyOn(RoomListStoreV3.instance, "createSection")
|
||||
.mockResolvedValue("element.io.section.work");
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
vm.createSection();
|
||||
expect(createSectionSpy).toHaveBeenCalled();
|
||||
});
|
||||
@@ -326,7 +346,7 @@ describe("RoomListHeaderViewModel", () => {
|
||||
describe("collapseOrExpandSections", () => {
|
||||
it("should dispatch RoomListCollapseAllSections when collapseSections is not 'expand'", () => {
|
||||
const fireSpy = jest.spyOn(defaultDispatcher, "fire");
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
|
||||
vm.collapseOrExpandSections();
|
||||
|
||||
@@ -335,7 +355,7 @@ describe("RoomListHeaderViewModel", () => {
|
||||
|
||||
it("should dispatch RoomListExpandAllSections when collapseSections is 'expand'", () => {
|
||||
const fireSpy = jest.spyOn(defaultDispatcher, "fire");
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
|
||||
// Drive the VM into the "expand" state by simulating all sections collapsed
|
||||
defaultDispatcher.dispatch(
|
||||
@@ -354,7 +374,7 @@ describe("RoomListHeaderViewModel", () => {
|
||||
|
||||
describe("RoomListSectionsCollapseStateChanged handling", () => {
|
||||
it("should set collapseSections to 'expand' when collapseSections is collapse", () => {
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
|
||||
defaultDispatcher.dispatch(
|
||||
{
|
||||
@@ -368,7 +388,7 @@ describe("RoomListHeaderViewModel", () => {
|
||||
});
|
||||
|
||||
it("should set collapseSections to 'collapse' when collapseSections is expand", () => {
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
|
||||
defaultDispatcher.dispatch(
|
||||
{
|
||||
@@ -382,7 +402,7 @@ describe("RoomListHeaderViewModel", () => {
|
||||
});
|
||||
|
||||
it("should set collapseSections to undefined when collapseSections is undefined", () => {
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
|
||||
// First drive it into a non-undefined state
|
||||
defaultDispatcher.dispatch(
|
||||
@@ -413,7 +433,7 @@ describe("RoomListHeaderViewModel", () => {
|
||||
});
|
||||
const setValueSpy = jest.spyOn(SettingsStore, "setValue").mockImplementation(jest.fn());
|
||||
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance });
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
expect(vm.getSnapshot().isMessagePreviewEnabled).toBe(true);
|
||||
|
||||
vm.toggleMessagePreview();
|
||||
@@ -421,5 +441,14 @@ describe("RoomListHeaderViewModel", () => {
|
||||
expect(setValueSpy).toHaveBeenCalledWith("RoomList.showMessagePreview", null, expect.anything(), false);
|
||||
expect(vm.getSnapshot().isMessagePreviewEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it("should call nextReleaseAnnouncement and set displaySectionReleaseAnnouncement to false when closeSectionReleaseAnnouncement is called", () => {
|
||||
vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore });
|
||||
|
||||
vm.closeSectionReleaseAnnouncement();
|
||||
|
||||
expect(ReleaseAnnouncementStore.instance.nextReleaseAnnouncement).toHaveBeenCalled();
|
||||
expect(vm.getSnapshot().displaySectionReleaseAnnouncement).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+36
-29
@@ -5,7 +5,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import EventEmitter from "events";
|
||||
import EventEmitter from "node:events";
|
||||
import {
|
||||
type MatrixClient,
|
||||
type MatrixEvent,
|
||||
@@ -30,8 +30,9 @@ import { Action } from "../../../src/dispatcher/actions";
|
||||
import { CallStore } from "../../../src/stores/CallStore";
|
||||
import { CallEvent, type Call } from "../../../src/models/Call";
|
||||
import { RoomListItemViewModel } from "../../../src/viewmodels/room-list/RoomListItemViewModel";
|
||||
import RoomListStoreV3, { CHATS_TAG } from "../../../src/stores/room-list-v3/RoomListStoreV3";
|
||||
import RoomListStoreV3 from "../../../src/stores/room-list-v3/RoomListStoreV3";
|
||||
import * as tagRoomModule from "../../../src/utils/room/tagRoom";
|
||||
import { CHATS_TAG } from "../../../src/stores/room-list-v3/section";
|
||||
|
||||
jest.mock("../../../src/viewmodels/room-list/utils", () => ({
|
||||
hasAccessToOptionsMenu: jest.fn().mockReturnValue(true),
|
||||
@@ -79,8 +80,10 @@ describe("RoomListItemViewModel", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "RoomList.showMessagePreview") return false;
|
||||
if (setting === "RoomList.OrderedCustomSections") return [];
|
||||
if (setting === "RoomList.CustomSectionData") return {};
|
||||
return false;
|
||||
});
|
||||
jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
||||
jest.spyOn(SettingsStore, "watchSetting").mockImplementation(() => "watcher-id");
|
||||
|
||||
jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue(null);
|
||||
@@ -514,20 +517,6 @@ describe("RoomListItemViewModel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("canMoveToSection", () => {
|
||||
it.each([
|
||||
[true, true],
|
||||
[false, false],
|
||||
])("should be %s when feature_room_list_sections is %s", (featureEnabled, expected) => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "feature_room_list_sections") return featureEnabled;
|
||||
return false;
|
||||
});
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
expect(viewModel.getSnapshot().canMoveToSection).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Actions", () => {
|
||||
it("should dispatch view room action on openRoom", () => {
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
@@ -636,10 +625,6 @@ describe("RoomListItemViewModel", () => {
|
||||
});
|
||||
|
||||
it("should include sections from orderedSectionTags excluding CHATS_TAG, favourite, and low priority", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "feature_room_list_sections") return true;
|
||||
return false;
|
||||
});
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
const sections = viewModel.getSnapshot().sections;
|
||||
@@ -648,10 +633,7 @@ describe("RoomListItemViewModel", () => {
|
||||
|
||||
it("should mark the room current section as selected", () => {
|
||||
room.tags = { [customTag]: { order: 0 } };
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "feature_room_list_sections") return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
|
||||
const sections = viewModel.getSnapshot().sections;
|
||||
@@ -660,7 +642,6 @@ describe("RoomListItemViewModel", () => {
|
||||
|
||||
it("should use custom section name from CustomSectionData", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "feature_room_list_sections") return true;
|
||||
if (setting === "RoomList.CustomSectionData")
|
||||
return { [customTag]: { name: "My Custom Section", tag: customTag } };
|
||||
return false;
|
||||
@@ -677,10 +658,6 @@ describe("RoomListItemViewModel", () => {
|
||||
if (setting === "RoomList.OrderedCustomSections") watchCallback = callback;
|
||||
return "watcher-id";
|
||||
});
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "feature_room_list_sections") return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
expect(viewModel.getSnapshot().sections).toHaveLength(1);
|
||||
@@ -695,6 +672,36 @@ describe("RoomListItemViewModel", () => {
|
||||
|
||||
expect(viewModel.getSnapshot().sections.map((s) => s.tag)).toEqual([]);
|
||||
});
|
||||
|
||||
it("should set areSectionsEnabled to true when RoomList.showSections is enabled", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "RoomList.showSections") return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
expect(viewModel.getSnapshot().areSectionsEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it("should update areSectionsEnabled when RoomList.showSections setting changes", () => {
|
||||
let watchCallback: CallbackFn<"RoomList.showSections"> = () => {};
|
||||
jest.spyOn(SettingsStore, "watchSetting").mockImplementation((setting, _room, callback) => {
|
||||
if (setting === "RoomList.showSections") watchCallback = callback;
|
||||
return "watcher-id";
|
||||
});
|
||||
|
||||
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||
expect(viewModel.getSnapshot().areSectionsEnabled).toBe(false);
|
||||
|
||||
// Enable sections
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "RoomList.showSections") return true;
|
||||
return false;
|
||||
});
|
||||
watchCallback("RoomList.showSections", null, null as any, null, null);
|
||||
|
||||
expect(viewModel.getSnapshot().areSectionsEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Cleanup", () => {
|
||||
@@ -13,6 +13,7 @@ import { shouldShowComponent } from "../../../src/customisations/helpers/UICompo
|
||||
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../src/LegacyCallHandler";
|
||||
import { TestSDKContext } from "../../unit-tests/TestSDKContext.ts";
|
||||
|
||||
jest.mock("../../../src/customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: jest.fn(),
|
||||
@@ -23,9 +24,12 @@ jest.mock("../../../src/PosthogTrackers", () => ({
|
||||
}));
|
||||
|
||||
describe("RoomListSearchViewModel", () => {
|
||||
const context = new TestSDKContext();
|
||||
|
||||
beforeEach(() => {
|
||||
mocked(shouldShowComponent).mockReturnValue(true);
|
||||
jest.spyOn(LegacyCallHandler.instance, "getSupportsPstnProtocol").mockReturnValue(false);
|
||||
context._LegacyCallHandler = new LegacyCallHandler(context);
|
||||
jest.spyOn(context._LegacyCallHandler, "getSupportsPstnProtocol").mockReturnValue(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -35,35 +39,50 @@ describe("RoomListSearchViewModel", () => {
|
||||
describe("snapshot", () => {
|
||||
it("should show explore button in Home space when UIComponent.ExploreRooms is enabled", () => {
|
||||
mocked(shouldShowComponent).mockReturnValue(true);
|
||||
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
|
||||
const vm = new RoomListSearchViewModel({
|
||||
activeSpace: MetaSpace.Home,
|
||||
legacyCallHandler: context.legacyCallHandler,
|
||||
});
|
||||
|
||||
expect(vm.getSnapshot().displayExploreButton).toBe(true);
|
||||
});
|
||||
|
||||
it("should hide explore button when not in Home space", () => {
|
||||
mocked(shouldShowComponent).mockReturnValue(true);
|
||||
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.VideoRooms });
|
||||
const vm = new RoomListSearchViewModel({
|
||||
activeSpace: MetaSpace.VideoRooms,
|
||||
legacyCallHandler: context.legacyCallHandler,
|
||||
});
|
||||
|
||||
expect(vm.getSnapshot().displayExploreButton).toBe(false);
|
||||
});
|
||||
|
||||
it("should hide explore button when UIComponent.ExploreRooms is disabled", () => {
|
||||
mocked(shouldShowComponent).mockReturnValue(false);
|
||||
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
|
||||
const vm = new RoomListSearchViewModel({
|
||||
activeSpace: MetaSpace.Home,
|
||||
legacyCallHandler: context.legacyCallHandler,
|
||||
});
|
||||
|
||||
expect(vm.getSnapshot().displayExploreButton).toBe(false);
|
||||
});
|
||||
|
||||
it("should show dial button when PSTN protocol is supported", () => {
|
||||
jest.spyOn(LegacyCallHandler.instance, "getSupportsPstnProtocol").mockReturnValue(true);
|
||||
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
|
||||
jest.spyOn(context.legacyCallHandler, "getSupportsPstnProtocol").mockReturnValue(true);
|
||||
const vm = new RoomListSearchViewModel({
|
||||
activeSpace: MetaSpace.Home,
|
||||
legacyCallHandler: context.legacyCallHandler,
|
||||
});
|
||||
|
||||
expect(vm.getSnapshot().displayDialButton).toBe(true);
|
||||
});
|
||||
|
||||
it("should hide dial button when PSTN protocol is not supported", () => {
|
||||
jest.spyOn(LegacyCallHandler.instance, "getSupportsPstnProtocol").mockReturnValue(false);
|
||||
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
|
||||
jest.spyOn(context.legacyCallHandler, "getSupportsPstnProtocol").mockReturnValue(false);
|
||||
const vm = new RoomListSearchViewModel({
|
||||
activeSpace: MetaSpace.Home,
|
||||
legacyCallHandler: context.legacyCallHandler,
|
||||
});
|
||||
|
||||
expect(vm.getSnapshot().displayDialButton).toBe(false);
|
||||
});
|
||||
@@ -72,7 +91,10 @@ describe("RoomListSearchViewModel", () => {
|
||||
describe("actions", () => {
|
||||
it("should fire OpenSpotlight action when onSearchClick is called", () => {
|
||||
const fireSpy = jest.spyOn(defaultDispatcher, "fire");
|
||||
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
|
||||
const vm = new RoomListSearchViewModel({
|
||||
activeSpace: MetaSpace.Home,
|
||||
legacyCallHandler: context.legacyCallHandler,
|
||||
});
|
||||
|
||||
vm.onSearchClick();
|
||||
expect(fireSpy).toHaveBeenCalledWith(Action.OpenSpotlight);
|
||||
@@ -80,7 +102,10 @@ describe("RoomListSearchViewModel", () => {
|
||||
|
||||
it("should fire OpenDialPad action when onDialPadClick is called", () => {
|
||||
const fireSpy = jest.spyOn(defaultDispatcher, "fire");
|
||||
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
|
||||
const vm = new RoomListSearchViewModel({
|
||||
activeSpace: MetaSpace.Home,
|
||||
legacyCallHandler: context.legacyCallHandler,
|
||||
});
|
||||
|
||||
vm.onDialPadClick();
|
||||
expect(fireSpy).toHaveBeenCalledWith(Action.OpenDialPad);
|
||||
@@ -88,7 +113,10 @@ describe("RoomListSearchViewModel", () => {
|
||||
|
||||
it("should fire ViewRoomDirectory action and track interaction when onExploreClick is called", () => {
|
||||
const fireSpy = jest.spyOn(defaultDispatcher, "fire");
|
||||
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
|
||||
const vm = new RoomListSearchViewModel({
|
||||
activeSpace: MetaSpace.Home,
|
||||
legacyCallHandler: context.legacyCallHandler,
|
||||
});
|
||||
|
||||
const mockEvent = {} as React.MouseEvent<HTMLButtonElement>;
|
||||
vm.onExploreClick(mockEvent);
|
||||
@@ -98,14 +126,17 @@ describe("RoomListSearchViewModel", () => {
|
||||
});
|
||||
|
||||
it("should update snapshot when PSTN protocol support changes", () => {
|
||||
jest.spyOn(LegacyCallHandler.instance, "getSupportsPstnProtocol").mockReturnValue(false);
|
||||
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
|
||||
jest.spyOn(context.legacyCallHandler, "getSupportsPstnProtocol").mockReturnValue(false);
|
||||
const vm = new RoomListSearchViewModel({
|
||||
activeSpace: MetaSpace.Home,
|
||||
legacyCallHandler: context.legacyCallHandler,
|
||||
});
|
||||
|
||||
expect(vm.getSnapshot().displayDialButton).toBe(false);
|
||||
|
||||
// Simulate PSTN protocol support change
|
||||
jest.spyOn(LegacyCallHandler.instance, "getSupportsPstnProtocol").mockReturnValue(true);
|
||||
LegacyCallHandler.instance.emit(LegacyCallHandlerEvent.ProtocolSupport);
|
||||
jest.spyOn(context.legacyCallHandler, "getSupportsPstnProtocol").mockReturnValue(true);
|
||||
context.legacyCallHandler.emit(LegacyCallHandlerEvent.ProtocolSupport);
|
||||
|
||||
expect(vm.getSnapshot().displayDialButton).toBe(true);
|
||||
|
||||
|
||||
@@ -6,29 +6,44 @@
|
||||
*/
|
||||
|
||||
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||
|
||||
import { RoomListSectionHeaderViewModel } from "../../../src/viewmodels/room-list/RoomListSectionHeaderViewModel";
|
||||
import { RoomNotificationState } from "../../../src/stores/notifications/RoomNotificationState";
|
||||
import { RoomNotificationStateStore } from "../../../src/stores/notifications/RoomNotificationStateStore";
|
||||
import { NotificationStateEvents } from "../../../src/stores/notifications/NotificationState";
|
||||
import { CallStore } from "../../../src/stores/CallStore";
|
||||
import { type Call } from "../../../src/models/Call";
|
||||
import { createTestClient, mkRoom } from "../../test-utils";
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
import RoomListStoreV3, { CHATS_TAG } from "../../../src/stores/room-list-v3/RoomListStoreV3";
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import RoomListStoreV3 from "../../../src/stores/room-list-v3/RoomListStoreV3";
|
||||
import { DefaultTagID } from "../../../src/stores/room-list-v3/skip-list/tag";
|
||||
import { CHATS_TAG, type SectionExpansionState } from "../../../src/stores/room-list-v3/section";
|
||||
|
||||
describe("RoomListSectionHeaderViewModel", () => {
|
||||
let onToggleExpanded: jest.Mock;
|
||||
let matrixClient: MatrixClient;
|
||||
// In-memory backing store shared between the getValue/setValue mocks so that
|
||||
// persisted expansion state round-trips within a test.
|
||||
let sectionExpansionState: SectionExpansionState;
|
||||
|
||||
beforeEach(() => {
|
||||
onToggleExpanded = jest.fn();
|
||||
matrixClient = createTestClient();
|
||||
sectionExpansionState = {};
|
||||
jest.spyOn(SettingsStore, "watchSetting").mockReturnValue("watcher-id");
|
||||
jest.spyOn(SettingsStore, "unwatchSetting").mockReturnValue(undefined);
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
|
||||
if (setting === "RoomList.OrderedCustomSections") return [];
|
||||
if (setting === "RoomList.SectionExpansionState") return sectionExpansionState;
|
||||
return null;
|
||||
});
|
||||
jest.spyOn(SettingsStore, "setValue").mockImplementation(async (setting, _roomId, _level, value) => {
|
||||
if (setting === "RoomList.SectionExpansionState") {
|
||||
sectionExpansionState = value as SectionExpansionState;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -96,6 +111,52 @@ describe("RoomListSectionHeaderViewModel", () => {
|
||||
expect(vm.isExpanded).toBe(false);
|
||||
});
|
||||
|
||||
it("should initialize expanded state from the persisted setting", () => {
|
||||
sectionExpansionState = { "!space:server": { "m.favourite": false } };
|
||||
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
|
||||
expect(vm.getSnapshot().isExpanded).toBe(false);
|
||||
});
|
||||
|
||||
it("should persist the expanded state at the device level on click", () => {
|
||||
const setValue = jest.spyOn(SettingsStore, "setValue");
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
|
||||
vm.onClick();
|
||||
|
||||
expect(setValue).toHaveBeenCalledWith("RoomList.SectionExpansionState", null, SettingLevel.DEVICE, {
|
||||
"!space:server": { "m.favourite": false },
|
||||
});
|
||||
expect(sectionExpansionState).toEqual({ "!space:server": { "m.favourite": false } });
|
||||
});
|
||||
|
||||
it("should persist the expanded state at the device level when set via the setter", () => {
|
||||
const setValue = jest.spyOn(SettingsStore, "setValue");
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
|
||||
vm.isExpanded = false;
|
||||
|
||||
expect(setValue).toHaveBeenCalledWith("RoomList.SectionExpansionState", null, SettingLevel.DEVICE, {
|
||||
"!space:server": { "m.favourite": false },
|
||||
});
|
||||
});
|
||||
|
||||
describe("displaySectionMenu", () => {
|
||||
it.each([
|
||||
[DefaultTagID.Favourite, false],
|
||||
@@ -113,6 +174,23 @@ describe("RoomListSectionHeaderViewModel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("canBeReordered", () => {
|
||||
it.each([
|
||||
[DefaultTagID.Favourite, false],
|
||||
[DefaultTagID.LowPriority, false],
|
||||
[CHATS_TAG, true],
|
||||
["element.io.section.custom", true],
|
||||
])("should be %s for tag %s", (tag, expected) => {
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag,
|
||||
title: "Section",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
expect(vm.getSnapshot().canBeReordered).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("onCustomSectionDataChange", () => {
|
||||
let watchCallback: () => void;
|
||||
|
||||
@@ -153,6 +231,19 @@ describe("RoomListSectionHeaderViewModel", () => {
|
||||
|
||||
expect(vm.getSnapshot().title).toBe("My Section");
|
||||
});
|
||||
|
||||
it("should not update title when tag is not a custom section tag", () => {
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
|
||||
watchCallback();
|
||||
|
||||
expect(vm.getSnapshot().title).toBe("Favourites");
|
||||
});
|
||||
});
|
||||
|
||||
describe("editSection", () => {
|
||||
@@ -216,11 +307,16 @@ describe("RoomListSectionHeaderViewModel", () => {
|
||||
let notificationState: RoomNotificationState;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
room = mkRoom(matrixClient, "!room:server");
|
||||
notificationState = new RoomNotificationState(room, false);
|
||||
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(notificationState);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("should set isUnread to false when no rooms have notifications", () => {
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
@@ -293,9 +389,234 @@ describe("RoomListSectionHeaderViewModel", () => {
|
||||
jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||
notificationState.emit(NotificationStateEvents.Update);
|
||||
|
||||
jest.advanceTimersByTime(200);
|
||||
expect(vm.getSnapshot().isUnread).toBe(true);
|
||||
});
|
||||
|
||||
describe("notification decoration", () => {
|
||||
it("should expose an empty decoration when no room has notifications", () => {
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
vm.setRooms([room]);
|
||||
|
||||
expect(vm.getSnapshot().notification).toEqual(
|
||||
expect.objectContaining({
|
||||
hasAnyNotificationOrActivity: false,
|
||||
isMention: false,
|
||||
isNotification: false,
|
||||
isUnsentMessage: false,
|
||||
isActivityNotification: false,
|
||||
count: 0,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not show the activity dot for an activity-only section", () => {
|
||||
jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||
jest.spyOn(notificationState, "isActivityNotification", "get").mockReturnValue(true);
|
||||
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
vm.setRooms([room]);
|
||||
|
||||
// Bold, but no badge to display
|
||||
expect(vm.getSnapshot().isUnread).toBe(true);
|
||||
expect(vm.getSnapshot().notification).toEqual(
|
||||
expect.objectContaining({
|
||||
hasAnyNotificationOrActivity: false,
|
||||
isActivityNotification: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should merge mentions, notifications and counts across rooms", () => {
|
||||
const room2 = mkRoom(matrixClient, "!room2:server");
|
||||
const notificationState2 = new RoomNotificationState(room2, false);
|
||||
|
||||
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState")
|
||||
.mockReturnValueOnce(notificationState)
|
||||
.mockReturnValue(notificationState2);
|
||||
|
||||
jest.spyOn(notificationState, "isMention", "get").mockReturnValue(true);
|
||||
jest.spyOn(notificationState, "count", "get").mockReturnValue(3);
|
||||
jest.spyOn(notificationState, "hasUnreadCount", "get").mockReturnValue(true);
|
||||
|
||||
jest.spyOn(notificationState2, "isNotification", "get").mockReturnValue(true);
|
||||
jest.spyOn(notificationState2, "count", "get").mockReturnValue(9);
|
||||
jest.spyOn(notificationState2, "hasUnreadCount", "get").mockReturnValue(true);
|
||||
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
vm.setRooms([room, room2]);
|
||||
|
||||
expect(vm.getSnapshot().notification).toEqual(
|
||||
expect.objectContaining({
|
||||
hasAnyNotificationOrActivity: true,
|
||||
isMention: true,
|
||||
isNotification: true,
|
||||
hasUnreadCount: true,
|
||||
count: 12,
|
||||
isActivityNotification: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should surface an unsent message from any room", () => {
|
||||
jest.spyOn(notificationState, "isUnsentMessage", "get").mockReturnValue(true);
|
||||
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
vm.setRooms([room]);
|
||||
|
||||
expect(vm.getSnapshot().notification).toEqual(
|
||||
expect.objectContaining({
|
||||
hasAnyNotificationOrActivity: true,
|
||||
isUnsentMessage: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should aggregate an invitation from any room", () => {
|
||||
jest.spyOn(notificationState, "invited", "get").mockReturnValue(true);
|
||||
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
vm.setRooms([room]);
|
||||
|
||||
expect(vm.getSnapshot().notification).toEqual(
|
||||
expect.objectContaining({
|
||||
hasAnyNotificationOrActivity: true,
|
||||
invited: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should aggregate an active call, preferring video over voice", () => {
|
||||
const room2 = mkRoom(matrixClient, "!room2:server");
|
||||
const notificationState2 = new RoomNotificationState(room2, false);
|
||||
|
||||
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState")
|
||||
.mockReturnValueOnce(notificationState)
|
||||
.mockReturnValue(notificationState2);
|
||||
|
||||
const voiceCall = {
|
||||
participants: new Map([["@a:server", new Set(["DEVICE"])]]),
|
||||
callType: CallType.Voice,
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
} as unknown as Call;
|
||||
const videoCall = {
|
||||
participants: new Map([["@b:server", new Set(["DEVICE"])]]),
|
||||
callType: CallType.Video,
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
} as unknown as Call;
|
||||
jest.spyOn(CallStore.instance, "getCall").mockImplementation((roomId) =>
|
||||
roomId === room.roomId ? voiceCall : videoCall,
|
||||
);
|
||||
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
vm.setRooms([room, room2]);
|
||||
|
||||
expect(vm.getSnapshot().notification).toEqual(
|
||||
expect.objectContaining({
|
||||
hasAnyNotificationOrActivity: true,
|
||||
callType: "video",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should ignore a call without participants", () => {
|
||||
const call = {
|
||||
participants: new Map(),
|
||||
callType: CallType.Video,
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
} as unknown as Call;
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(call);
|
||||
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
vm.setRooms([room]);
|
||||
|
||||
expect(vm.getSnapshot().notification?.callType).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should show a notification without a count badge for a mark-as-unread room", () => {
|
||||
// "Mark as unread" sets level=Notification with count=0 (no real notification events).
|
||||
jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
|
||||
jest.spyOn(notificationState, "isNotification", "get").mockReturnValue(true);
|
||||
jest.spyOn(notificationState, "count", "get").mockReturnValue(0);
|
||||
jest.spyOn(notificationState, "hasUnreadCount", "get").mockReturnValue(false);
|
||||
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
vm.setRooms([room]);
|
||||
|
||||
expect(vm.getSnapshot().isUnread).toBe(true);
|
||||
expect(vm.getSnapshot().notification).toEqual(
|
||||
expect.objectContaining({
|
||||
hasAnyNotificationOrActivity: true,
|
||||
isNotification: true,
|
||||
hasUnreadCount: false,
|
||||
// The || 1 fallback gives a count of 1 even though no real count exists
|
||||
count: 1,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should update the decoration when a notification state update event fires", () => {
|
||||
const vm = new RoomListSectionHeaderViewModel({
|
||||
tag: "m.favourite",
|
||||
title: "Favourites",
|
||||
spaceId: "!space:server",
|
||||
onToggleExpanded,
|
||||
});
|
||||
vm.setRooms([room]);
|
||||
|
||||
expect(vm.getSnapshot().notification?.isMention).toBe(false);
|
||||
|
||||
jest.spyOn(notificationState, "isMention", "get").mockReturnValue(true);
|
||||
notificationState.emit(NotificationStateEvents.Update);
|
||||
|
||||
jest.advanceTimersByTime(200);
|
||||
expect(vm.getSnapshot().notification?.isMention).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("should unsubscribe from all notification states on dispose", () => {
|
||||
jest.spyOn(notificationState, "off");
|
||||
|
||||
|
||||
+795
-123
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user