Room list: persist section state (expanded/collapsed) (#34351)
* Add new setting to store section state * Store section state in device settings * Wire up section header view model and new section functions * Update room list vm tests * Add e2e test on section state persistence * Use more intuitive syntax for SectionExpansionState * Formatting * Fix chats section staying expanded * Add CHAT section case in e2e test
This commit is contained in:
@@ -16,11 +16,14 @@ import {
|
||||
getCustomSectionData,
|
||||
getOrderedCustomSections,
|
||||
isDefaultSectionTag,
|
||||
isSectionExpanded,
|
||||
setSectionExpanded,
|
||||
CHATS_TAG,
|
||||
CUSTOM_SECTION_TAG_PREFIX,
|
||||
isSectionTag,
|
||||
reorderSection,
|
||||
} from "../../../../src/stores/room-list-v3/section";
|
||||
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
||||
import { CreateSectionDialog } from "../../../../src/components/views/dialogs/CreateSectionDialog";
|
||||
import { RemoveSectionDialog } from "../../../../src/components/views/dialogs/RemoveSectionDialog";
|
||||
import { DefaultTagID } from "../../../../src/stores/room-list-v3/skip-list/tag";
|
||||
@@ -131,6 +134,63 @@ describe("section", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("isSectionExpanded", () => {
|
||||
const spaceId = "!space:server";
|
||||
const tag = "element.io.section.abc";
|
||||
|
||||
it.each([
|
||||
{ value: {}, result: true },
|
||||
{ value: { "!other:server": { [tag]: false } }, result: true },
|
||||
{ value: { [spaceId]: { "other.tag": false } }, result: true },
|
||||
{ value: { [spaceId]: { [tag]: false } }, result: false },
|
||||
])("returns the persisted state=$result when value=$value", ({ value, result }) => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(value);
|
||||
expect(isSectionExpanded(spaceId, tag)).toBe(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setSectionExpanded", () => {
|
||||
const spaceId = "!space:server";
|
||||
const tag = "element.io.section.abc";
|
||||
|
||||
it("persists the state at the device level", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue({});
|
||||
const setValueSpy = jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
||||
|
||||
await setSectionExpanded(spaceId, tag, false);
|
||||
|
||||
expect(setValueSpy).toHaveBeenCalledWith("RoomList.SectionExpansionState", null, SettingLevel.DEVICE, {
|
||||
[spaceId]: { [tag]: false },
|
||||
});
|
||||
});
|
||||
|
||||
it("merges with existing state for other spaces and tags", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue({
|
||||
"!other:server": { "other.tag": false },
|
||||
[spaceId]: { "existing.tag": true },
|
||||
});
|
||||
const setValueSpy = jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
||||
|
||||
await setSectionExpanded(spaceId, tag, false);
|
||||
|
||||
expect(setValueSpy).toHaveBeenCalledWith("RoomList.SectionExpansionState", null, SettingLevel.DEVICE, {
|
||||
"!other:server": { "other.tag": false },
|
||||
[spaceId]: { "existing.tag": true, [tag]: false },
|
||||
});
|
||||
});
|
||||
|
||||
it("overwrites the previous state for the same space and tag", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue({ [spaceId]: { [tag]: false } });
|
||||
const setValueSpy = jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
||||
|
||||
await setSectionExpanded(spaceId, tag, true);
|
||||
|
||||
expect(setValueSpy).toHaveBeenCalledWith("RoomList.SectionExpansionState", null, SettingLevel.DEVICE, {
|
||||
[spaceId]: { [tag]: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("createSection", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(null);
|
||||
|
||||
@@ -16,23 +16,34 @@ 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 { 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 } from "../../../src/stores/room-list-v3/section";
|
||||
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(() => {
|
||||
@@ -100,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],
|
||||
|
||||
@@ -29,7 +29,11 @@ import { DefaultTagID } from "../../../src/stores/room-list-v3/skip-list/tag";
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
import { tagRoom } from "../../../src/utils/room/tagRoom";
|
||||
import { getSectionTagForRoom } from "../../../src/utils/room/getSectionTagForRoom";
|
||||
import { CHATS_TAG, CUSTOM_SECTION_TAG_PREFIX } from "../../../src/stores/room-list-v3/section";
|
||||
import {
|
||||
CHATS_TAG,
|
||||
CUSTOM_SECTION_TAG_PREFIX,
|
||||
type SectionExpansionState,
|
||||
} from "../../../src/stores/room-list-v3/section";
|
||||
import { MetaSpace } from "../../../src/stores/spaces";
|
||||
import { RoomNotificationStateStore } from "../../../src/stores/notifications/RoomNotificationStateStore";
|
||||
import { type RoomNotificationState } from "../../../src/stores/notifications/RoomNotificationState";
|
||||
@@ -55,9 +59,27 @@ describe("RoomListViewModel", () => {
|
||||
let room2: Room;
|
||||
let room3: Room;
|
||||
let viewModel: RoomListViewModel;
|
||||
// In-memory backing store for the persisted section expansion setting, reset each test so
|
||||
// collapse state does not leak between tests and writes round-trip synchronously.
|
||||
let sectionExpansionState: SectionExpansionState;
|
||||
|
||||
beforeEach(() => {
|
||||
matrixClient = createTestClient();
|
||||
|
||||
sectionExpansionState = {};
|
||||
const realGetValue = SettingsStore.getValue.bind(SettingsStore);
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting, roomId, excludeDefault) => {
|
||||
if (setting === "RoomList.SectionExpansionState") return sectionExpansionState;
|
||||
return realGetValue(setting, roomId, excludeDefault);
|
||||
});
|
||||
const realSetValue = SettingsStore.setValue.bind(SettingsStore);
|
||||
jest.spyOn(SettingsStore, "setValue").mockImplementation(async (setting, roomId, level, value) => {
|
||||
if (setting === "RoomList.SectionExpansionState") {
|
||||
sectionExpansionState = value as SectionExpansionState;
|
||||
return;
|
||||
}
|
||||
return realSetValue(setting, roomId, level, value);
|
||||
});
|
||||
sdkContext = new TestSDKContext();
|
||||
sdkContext._client = matrixClient;
|
||||
room1 = mkStubRoom("!room1:server", "Room 1", matrixClient);
|
||||
@@ -428,6 +450,7 @@ describe("RoomListViewModel", () => {
|
||||
if (setting === "RoomList.showSections") return showSections;
|
||||
if (setting === "RoomList.CustomSectionData") return {};
|
||||
if (setting === "RoomList.OrderedCustomSections") return [];
|
||||
if (setting === "RoomList.SectionExpansionState") return {};
|
||||
return undefined as any;
|
||||
});
|
||||
}
|
||||
@@ -465,6 +488,7 @@ describe("RoomListViewModel", () => {
|
||||
if (setting === "RoomList.showSections") return showSections;
|
||||
if (setting === "RoomList.CustomSectionData") return {};
|
||||
if (setting === "RoomList.OrderedCustomSections") return [];
|
||||
if (setting === "RoomList.SectionExpansionState") return {};
|
||||
return undefined as any;
|
||||
});
|
||||
jest.spyOn(SettingsStore, "watchSetting").mockImplementation((setting, _room, callback) => {
|
||||
|
||||
Reference in New Issue
Block a user