Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Spaces.includeSubSpaceRoomsInRoomList settings #7682

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,

static SPACES_SETTINGS = [
"Spaces.allRoomsInHome",
"Spaces.includeSubSpaceRoomsInRoomList",
];

static KEYBINDINGS_SETTINGS = [
Expand Down
1 change: 1 addition & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,7 @@
"Show chat effects (animations when receiving e.g. confetti)": "Show chat effects (animations when receiving e.g. confetti)",
"Show all rooms in Home": "Show all rooms in Home",
"All rooms you're in will appear in Home.": "All rooms you're in will appear in Home.",
"Display all people and rooms from nested spaces in the room list for a space": "Display all people and rooms from nested spaces in the room list for a space",
"Developer mode": "Developer mode",
"Automatically send debug logs on any error": "Automatically send debug logs on any error",
"Automatically send debug logs on decryption errors": "Automatically send debug logs on decryption errors",
Expand Down
5 changes: 5 additions & 0 deletions src/settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,11 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: [SettingLevel.ROOM_ACCOUNT],
default: true,
},
"Spaces.includeSubSpaceRoomsInRoomList": {
displayName: _td("Display all people and rooms from nested spaces in the room list for a space"),
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: true,
},
"developerMode": {
displayName: _td("Developer mode"),
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
Expand Down
15 changes: 12 additions & 3 deletions src/stores/room-list/filters/SpaceFilterCondition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,31 +34,40 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi
private roomIds = new Set<string>();
private userIds = new Set<string>();
private showPeopleInSpace = true;
private showSubSpaceRoomsInSpace = true;
private space: SpaceKey = MetaSpace.Home;

public get kind(): FilterKind {
return FilterKind.Prefilter;
}

public isVisible(room: Room): boolean {
return SpaceStore.instance.isRoomInSpace(this.space, room.roomId);
return SpaceStore.instance.isRoomInSpace(this.space, room.roomId, this.showSubSpaceRoomsInSpace);
}

private onStoreUpdate = async (forceUpdate = false): Promise<void> => {
const beforeShowSubSpaceRoomsInSpace = this.showSubSpaceRoomsInSpace;
this.showSubSpaceRoomsInSpace = SettingsStore.getValue("Spaces.includeSubSpaceRoomsInRoomList", this.space);

const beforeRoomIds = this.roomIds;
// clone the set as it may be mutated by the space store internally
this.roomIds = new Set(SpaceStore.instance.getSpaceFilteredRoomIds(this.space));
this.roomIds = new Set(
SpaceStore.instance.getSpaceFilteredRoomIds(this.space, this.showSubSpaceRoomsInSpace, true),
);

const beforeUserIds = this.userIds;
// clone the set as it may be mutated by the space store internally
this.userIds = new Set(SpaceStore.instance.getSpaceFilteredUserIds(this.space));
this.userIds = new Set(
SpaceStore.instance.getSpaceFilteredUserIds(this.space, this.showSubSpaceRoomsInSpace, true),
);

const beforeShowPeopleInSpace = this.showPeopleInSpace;
this.showPeopleInSpace = isMetaSpace(this.space[0]) ||
SettingsStore.getValue("Spaces.showPeopleInSpace", this.space);

if (forceUpdate ||
beforeShowPeopleInSpace !== this.showPeopleInSpace ||
beforeShowSubSpaceRoomsInSpace !== this.showSubSpaceRoomsInSpace ||
setHasDiff(beforeRoomIds, this.roomIds) ||
setHasDiff(beforeUserIds, this.userIds)
) {
Expand Down
21 changes: 19 additions & 2 deletions src/stores/spaces/SpaceStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
SettingsStore.monitorSetting("Spaces.allRoomsInHome", null);
SettingsStore.monitorSetting("Spaces.enabledMetaSpaces", null);
SettingsStore.monitorSetting("Spaces.showPeopleInSpace", null);
// @TODO kerry handle switching spaces if needed when this changes?
SettingsStore.monitorSetting("Spaces.includeSubSpaceRoomsInRoomList", null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this monitoring a setting which it doesn't react to changes of?

}

public get invitedSpaces(): Room[] {
Expand Down Expand Up @@ -814,15 +816,30 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
// try to find the canonical parent first
let parent: SpaceKey = this.getCanonicalParent(roomId)?.roomId;

const includeSubSpaceRoomsInRoomList = SettingsStore.getValue("Spaces.includeSubSpaceRoomsInRoomList");
console.log('PPPP', roomId, parent, includeSubSpaceRoomsInRoomList);

// otherwise, if subspaces rooms are not aggregated
// use the first known direct parent of the room
if (!parent && !includeSubSpaceRoomsInRoomList) {
const parents = this.getKnownParents(roomId);

// use the first known parent
parent = parents.values().next().value;
}

// otherwise, try to find a root space which contains this room
if (!parent) {
parent = this.rootSpaces.find(s => this.isRoomInSpace(s.roomId, roomId))?.roomId;
parent = this.rootSpaces.find(s =>
this.isRoomInSpace(s.roomId, roomId, includeSubSpaceRoomsInRoomList))?.roomId;
}

// otherwise, try to find a metaspace which contains this room
if (!parent) {
// search meta spaces in reverse as Home is the first and least specific one
parent = [...this.enabledMetaSpaces].reverse().find(s => this.isRoomInSpace(s, roomId));
parent = [...this.enabledMetaSpaces].reverse().find(s =>
this.isRoomInSpace(s, roomId, includeSubSpaceRoomsInRoomList),
);
}

// don't trigger a context switch when we are switching a space to match the chosen room
Expand Down
153 changes: 109 additions & 44 deletions test/stores/SpaceStore-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1000,10 +1000,12 @@ describe("SpaceStore", () => {
});

describe("space auto switching tests", () => {
beforeEach(async () => {
[room1, room2, room3, orphan1].forEach(mkRoom);
const setupStore = async () => {
[room1, room2, room3, room4, orphan1].forEach(mkRoom);
mkSpace(space4, [room1]);
mkSpace(space3, [room4, space4]);
mkSpace(space1, [room1, room2, room3]);
mkSpace(space2, [room1, room2]);
mkSpace(space2, [room1, room2, space3]);

const cliRoom2 = client.getRoom(room2);
mocked(cliRoom2.currentState).getStateEvents.mockImplementation(testUtils.mockStateEventImplementation([
Expand All @@ -1018,55 +1020,118 @@ describe("SpaceStore", () => {
}),
]));
await run();
});
};

it("no switch required, room is in current space", async () => {
viewRoom(room1);
store.setActiveSpace(space1, false);
viewRoom(room2);
expect(store.activeSpace).toBe(space1);
});
describe('when includeSubSpaceRoomsInRoomList is true', () => {
beforeEach(async () => {
await setupStore();
});

it("switch to canonical parent space for room", async () => {
viewRoom(room1);
store.setActiveSpace(space2, false);
viewRoom(room2);
expect(store.activeSpace).toBe(space2);
});
it("no switch required, room is in current space", async () => {
viewRoom(room1);
store.setActiveSpace(space1, false);
viewRoom(room2);
expect(store.activeSpace).toBe(space1);
});

it("switch to first containing space for room", async () => {
viewRoom(room2);
store.setActiveSpace(space2, false);
viewRoom(room3);
expect(store.activeSpace).toBe(space1);
});
it("switch to canonical parent space for room", async () => {
viewRoom(room1);
store.setActiveSpace(space2, false);
viewRoom(room2);
expect(store.activeSpace).toBe(space2);
});

it("switch to other rooms for orphaned room", async () => {
viewRoom(room1);
store.setActiveSpace(space1, false);
viewRoom(orphan1);
expect(store.activeSpace).toBe(MetaSpace.Orphans);
});
it("switch to first containing space for room", async () => {
viewRoom(room2);
store.setActiveSpace(space2, false);
viewRoom(room3);
expect(store.activeSpace).toBe(space1);
});

it("switch to first valid space when selected metaspace is disabled", async () => {
store.setActiveSpace(MetaSpace.People, false);
expect(store.activeSpace).toBe(MetaSpace.People);
await SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.DEVICE, {
[MetaSpace.Home]: false,
[MetaSpace.Favourites]: true,
[MetaSpace.People]: false,
[MetaSpace.Orphans]: true,
it("switches to first root space containing the room", async () => {
viewRoom(room2);
store.setActiveSpace(space1, false);
// space2 > space3 > room4
viewRoom(room4);
expect(store.activeSpace).toBe(space2);
});

it("switch to other rooms for orphaned room", async () => {
viewRoom(room1);
store.setActiveSpace(space1, false);
viewRoom(orphan1);
expect(store.activeSpace).toBe(MetaSpace.Orphans);
});

it("switch to first valid space when selected metaspace is disabled", async () => {
store.setActiveSpace(MetaSpace.People, false);
expect(store.activeSpace).toBe(MetaSpace.People);
await SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.DEVICE, {
[MetaSpace.Home]: false,
[MetaSpace.Favourites]: true,
[MetaSpace.People]: false,
[MetaSpace.Orphans]: true,
});
jest.runAllTimers();
expect(store.activeSpace).toBe(MetaSpace.Orphans);
});

it("when switching rooms in the all rooms home space don't switch to related space", async () => {
await setShowAllRooms(true);
viewRoom(room2);
store.setActiveSpace(MetaSpace.Home, false);
viewRoom(room1);
expect(store.activeSpace).toBe(MetaSpace.Home);
});
jest.runAllTimers();
expect(store.activeSpace).toBe(MetaSpace.Orphans);
});

it("when switching rooms in the all rooms home space don't switch to related space", async () => {
await setShowAllRooms(true);
viewRoom(room2);
store.setActiveSpace(MetaSpace.Home, false);
viewRoom(room1);
expect(store.activeSpace).toBe(MetaSpace.Home);
describe('when includeSubSpaceRoomsInRoomList is false', () => {
beforeAll(() => {
SettingsStore.setValue(
'Spaces.includeSubSpaceRoomsInRoomList', undefined, SettingLevel.ACCOUNT, false,
);
});

afterAll(() => {
SettingsStore.setValue(
'Spaces.includeSubSpaceRoomsInRoomList', undefined, SettingLevel.ACCOUNT, true,
);
});

beforeEach(async () => {
await setupStore();
});

it("no switch required, room is in current space", async () => {
expect(SettingsStore.getValue('Spaces.includeSubSpaceRoomsInRoomList')).toEqual(false);
viewRoom(room1);
store.setActiveSpace(space1, false);
viewRoom(room2);
expect(store.activeSpace).toBe(space1);
});

it("switches to canonical parent space for room when viewing room", async () => {
viewRoom(room1);
store.setActiveSpace(space2, false);
viewRoom(room2);
expect(store.activeSpace).toBe(space2);
});

it("switches to first parent space for room when viewing room", async () => {
// differs from behaviour when includeSubSpaceRoomsInRoomList is truthy
viewRoom(room2);
store.setActiveSpace(space1, false);
// space2 > space3 > room4
viewRoom(room4);
expect(store.activeSpace).toBe(space3);
});

it("switch to other rooms for orphaned room", async () => {
viewRoom(room1);
store.setActiveSpace(space1, false);
viewRoom(orphan1);
expect(store.activeSpace).toBe(MetaSpace.Orphans);
});
});
});

Expand Down
61 changes: 58 additions & 3 deletions test/stores/room-list/filters/SpaceFilterCondition-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ describe('SpaceFilterCondition', () => {
beforeEach(() => {
jest.resetAllMocks();
SettingsStoreMock.getValue.mockClear().mockImplementation(makeMockGetValue());
SpaceStoreInstanceMock.getSpaceFilteredRoomIds.mockReturnValue(new Set([]));
SpaceStoreInstanceMock.getSpaceFilteredUserIds.mockReturnValue(new Set([]));
SpaceStoreInstanceMock.isRoomInSpace.mockReturnValue(true);
});
Expand All @@ -67,11 +68,26 @@ describe('SpaceFilterCondition', () => {

describe('isVisible', () => {
const room1 = { roomId: room1Id } as unknown as Room;
it('calls isRoomInSpace correctly', () => {
it('calls isRoomInSpace correctly when showSubSpaceRoomsInSpace is truthy', () => {
SettingsStoreMock.getValue.mockImplementation(makeMockGetValue({
["Spaces.includeSubSpaceRoomsInRoomList"]: { [space1]: true },
}));

const filter = initFilter(space1);

expect(filter.isVisible(room1)).toEqual(true);
expect(SpaceStoreInstanceMock.isRoomInSpace).toHaveBeenCalledWith(space1, room1Id, true);
});

it('calls isRoomInSpace correctly when showSubSpaceRoomsInSpace is truthy', () => {
SettingsStoreMock.getValue.mockImplementation(makeMockGetValue({
["Spaces.includeSubSpaceRoomsInRoomList"]: { [space1]: false },
}));

const filter = initFilter(space1);

expect(filter.isVisible(room1)).toEqual(true);
expect(SpaceStoreInstanceMock.isRoomInSpace).toHaveBeenCalledWith(space1, room1Id);
expect(SpaceStoreInstanceMock.isRoomInSpace).toHaveBeenCalledWith(space1, room1Id, false);
});
});

Expand All @@ -84,6 +100,45 @@ describe('SpaceFilterCondition', () => {
expect(emitSpy).toHaveBeenCalledWith(FILTER_CHANGED);
});

it('compares sub space rooms and dms when Spaces.includeSubSpaceRoomsInRoomList enabled', async () => {
SettingsStoreMock.getValue.mockImplementation(makeMockGetValue({
["Spaces.includeSubSpaceRoomsInRoomList"]: { [space1]: true },
}));
const filter = new SpaceFilterCondition();
filter.updateSpace(space1);
jest.runOnlyPendingTimers();
expect(SpaceStoreInstanceMock.getSpaceFilteredRoomIds).toHaveBeenCalledWith(space1, true, true);
expect(SpaceStoreInstanceMock.getSpaceFilteredUserIds).toHaveBeenCalledWith(space1, true, true);
});

it('compares only direct child rooms and dms when Spaces.includeSubSpaceRoomsInRoomList disabled', async () => {
SettingsStoreMock.getValue.mockImplementation(makeMockGetValue({
["Spaces.includeSubSpaceRoomsInRoomList"]: { [space1]: false },
}));
const filter = new SpaceFilterCondition();
filter.updateSpace(space1);
jest.runOnlyPendingTimers();
expect(SpaceStoreInstanceMock.getSpaceFilteredRoomIds).toHaveBeenCalledWith(space1, false, true);
expect(SpaceStoreInstanceMock.getSpaceFilteredUserIds).toHaveBeenCalledWith(space1, false, true);
});

it('emits filter changed event when Spaces.includeSubSpaceRoomsInRoomList setting changes', async () => {
// init filter with setting true for space1
SettingsStoreMock.getValue.mockImplementation(makeMockGetValue({
["Spaces.includeSubSpaceRoomsInRoomList"]: { [space1]: true },
}));
const filter = initFilter(space1);
const emitSpy = jest.spyOn(filter, 'emit');

SettingsStoreMock.getValue.mockImplementation(makeMockGetValue({
["Spaces.includeSubSpaceRoomsInRoomList"]: { [space1]: false },
}));

SpaceStoreInstanceMock.emit(space1);
jest.runOnlyPendingTimers();
expect(emitSpy).toHaveBeenCalledWith(FILTER_CHANGED);
});

describe('showPeopleInSpace setting', () => {
it('emits filter changed event when setting changes', async () => {
// init filter with setting true for space1
Expand Down Expand Up @@ -152,7 +207,7 @@ describe('SpaceFilterCondition', () => {
expect(emitSpy).not.toHaveBeenCalledWith(FILTER_CHANGED);
});

describe('when directChildRoomIds change', () => {
describe('when spaceFilteredRoomIds change', () => {
beforeEach(() => {
SpaceStoreInstanceMock.getSpaceFilteredRoomIds.mockReturnValue(new Set([room1Id, room2Id]));
});
Expand Down