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

Commit 5ba6949

Browse files
committed
Hide ignored invites
With this PR, invites that are specified by MSC3847 to be ignored are hidden.
1 parent 4c4a63f commit 5ba6949

File tree

7 files changed

+147
-47
lines changed

7 files changed

+147
-47
lines changed

src/stores/notifications/RoomNotificationStateStore.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -108,32 +108,46 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
108108
return RoomNotificationStateStore.internalInstance;
109109
}
110110

111-
private onSync = (state: SyncState, prevState?: SyncState, data?: ISyncStateData) => {
111+
private onSync = async (state: SyncState, prevState?: SyncState, data?: ISyncStateData) => {
112112
// Only count visible rooms to not torment the user with notification counts in rooms they can't see.
113113
// This will include highlights from the previous version of the room internally
114+
115+
// Async phase: gather data. Do *not* perform any side-effect.
114116
const globalState = new SummarizedNotificationState();
115117
const visibleRooms = this.matrixClient.getVisibleRooms();
116118

117119
let numFavourites = 0;
118120
for (const room of visibleRooms) {
119-
if (VisibilityProvider.instance.isRoomVisible(room)) {
121+
if (await VisibilityProvider.instance.isRoomVisible(room)) {
120122
globalState.add(this.getRoomState(room));
121123

122124
if (room.tags[DefaultTagID.Favourite] && !room.getType()) numFavourites++;
123125
}
124126
}
125127

126-
PosthogAnalytics.instance.setProperty("numFavouriteRooms", numFavourites);
127-
128-
if (this.globalState.symbol !== globalState.symbol ||
129-
this.globalState.count !== globalState.count ||
130-
this.globalState.color !== globalState.color ||
131-
this.globalState.numUnreadStates !== globalState.numUnreadStates ||
132-
state !== prevState
133-
) {
134-
this._globalState = globalState;
135-
this.emit(UPDATE_STATUS_INDICATOR, globalState, state, prevState, data);
136-
}
128+
// Sync phrase: perform side-effects.
129+
// By making sure that we perform side-effects after the last call to `await`, we make sure that
130+
// the side-effects represent *some* snapshot of reality, rather than a mix of two ore more
131+
// snapshots.
132+
//
133+
// Normally, calls to `VisibilityProvider.instance.isRoomVisible` should resolve in the order
134+
// in which they have been enqueued. As long as this holds, we have guaranteeds that the side-
135+
// effects we're causing correspond to the latest snapshot of reality.
136+
(() => {
137+
// Do NOT make this function `async`.
138+
// Its sole purpose is to make sure that we do not call `await` while performing side-effects.
139+
PosthogAnalytics.instance.setProperty("numFavouriteRooms", numFavourites);
140+
141+
if (this.globalState.symbol !== globalState.symbol ||
142+
this.globalState.count !== globalState.count ||
143+
this.globalState.color !== globalState.color ||
144+
this.globalState.numUnreadStates !== globalState.numUnreadStates ||
145+
state !== prevState
146+
) {
147+
this._globalState = globalState;
148+
this.emit(UPDATE_STATUS_INDICATOR, globalState, state, prevState, data);
149+
}
150+
})();
137151
};
138152

139153
protected async onReady() {

src/stores/room-list/RoomListStore.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> implements
318318
await VisibilityProvider.instance.onNewInvitedRoom(room);
319319
}
320320

321-
if (!VisibilityProvider.instance.isRoomVisible(room)) {
321+
if (!await VisibilityProvider.instance.isRoomVisible(room)) {
322322
return; // don't do anything on rooms that aren't visible
323323
}
324324

@@ -342,7 +342,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> implements
342342
this.algorithm.updatesInhibited = true;
343343

344344
// Figure out which rooms are about to be valid, and the state of affairs
345-
const rooms = this.getPlausibleRooms();
345+
const rooms = await this.getPlausibleRooms();
346346
const currentSticky = this.algorithm.stickyRoom;
347347
const stickyIsStillPresent = currentSticky && rooms.includes(currentSticky);
348348

@@ -486,10 +486,16 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> implements
486486
this.updateFn.trigger();
487487
};
488488

489-
private getPlausibleRooms(): Room[] {
489+
private async getPlausibleRooms(): Promise<Room[]> {
490490
if (!this.matrixClient) return [];
491491

492-
let rooms = this.matrixClient.getVisibleRooms().filter(r => VisibilityProvider.instance.isRoomVisible(r));
492+
const allRooms = this.matrixClient.getVisibleRooms();
493+
let rooms = [];
494+
for (const room of allRooms) {
495+
if (await VisibilityProvider.instance.isRoomVisible(room)) {
496+
rooms.push(allRooms);
497+
}
498+
}
493499

494500
if (this.prefilterConditions.length > 0) {
495501
rooms = rooms.filter(r => {
@@ -513,10 +519,10 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> implements
513519
* @param trigger Set to false to prevent a list update from being sent. Should only
514520
* be used if the calling code will manually trigger the update.
515521
*/
516-
public regenerateAllLists({ trigger = true }) {
522+
public async regenerateAllLists({ trigger = true }) {
517523
logger.warn("Regenerating all room lists");
518524

519-
const rooms = this.getPlausibleRooms();
525+
const rooms = await this.getPlausibleRooms();
520526

521527
const sorts: ITagSortingMap = {};
522528
const orders: IListOrderingMap = {};
@@ -528,8 +534,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> implements
528534
RoomListLayoutStore.instance.ensureLayoutExists(tagId);
529535
}
530536

531-
this.algorithm.populateTags(sorts, orders);
532-
this.algorithm.setKnownRooms(rooms);
537+
await this.algorithm.populateTags(sorts, orders);
538+
await this.algorithm.setKnownRooms(rooms);
533539

534540
this.initialListsGenerated = true;
535541

src/stores/room-list/algorithms/Algorithm.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -163,18 +163,18 @@ export class Algorithm extends EventEmitter {
163163
this.recalculateActiveCallRooms(tagId);
164164
}
165165

166-
private updateStickyRoom(val: Room) {
167-
this.doUpdateStickyRoom(val);
166+
private async updateStickyRoom(val: Room) {
167+
await this.doUpdateStickyRoom(val);
168168
this._lastStickyRoom = null; // clear to indicate we're done changing
169169
}
170170

171-
private doUpdateStickyRoom(val: Room) {
171+
private async doUpdateStickyRoom(val: Room) {
172172
if (val?.isSpaceRoom() && val.getMyMembership() !== "invite") {
173173
// no-op sticky rooms for spaces - they're effectively virtual rooms
174174
val = null;
175175
}
176176

177-
if (val && !VisibilityProvider.instance.isRoomVisible(val)) {
177+
if (val && !await VisibilityProvider.instance.isRoomVisible(val)) {
178178
val = null; // the room isn't visible - lie to the rest of this function
179179
}
180180

@@ -402,7 +402,7 @@ export class Algorithm extends EventEmitter {
402402
* @param {ITagSortingMap} tagSortingMap The tags to generate.
403403
* @param {IListOrderingMap} listOrderingMap The ordering of those tags.
404404
*/
405-
public populateTags(tagSortingMap: ITagSortingMap, listOrderingMap: IListOrderingMap): void {
405+
public async populateTags(tagSortingMap: ITagSortingMap, listOrderingMap: IListOrderingMap): Promise<void> {
406406
if (!tagSortingMap) throw new Error(`Sorting map cannot be null or empty`);
407407
if (!listOrderingMap) throw new Error(`Ordering ma cannot be null or empty`);
408408
if (arrayHasDiff(Object.keys(tagSortingMap), Object.keys(listOrderingMap))) {
@@ -442,7 +442,7 @@ export class Algorithm extends EventEmitter {
442442
* previously known information and instead use these rooms instead.
443443
* @param {Room[]} rooms The rooms to force the algorithm to use.
444444
*/
445-
public setKnownRooms(rooms: Room[]): void {
445+
public async setKnownRooms(rooms: Room[]): Promise<void> {
446446
if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`);
447447
if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`);
448448

@@ -456,7 +456,9 @@ export class Algorithm extends EventEmitter {
456456
// Before we go any further we need to clear (but remember) the sticky room to
457457
// avoid accidentally duplicating it in the list.
458458
const oldStickyRoom = this._stickyRoom;
459-
if (oldStickyRoom) this.updateStickyRoom(null);
459+
if (oldStickyRoom) {
460+
await this.updateStickyRoom(null);
461+
}
460462

461463
this.rooms = rooms;
462464

src/stores/room-list/filters/VisibilityProvider.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import LegacyCallHandler from "../../../LegacyCallHandler";
2020
import { RoomListCustomisations } from "../../../customisations/RoomList";
2121
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
2222
import VoipUserMapper from "../../../VoipUserMapper";
23+
import { MatrixClientPeg } from "../../../MatrixClientPeg";
2324

2425
export class VisibilityProvider {
2526
private static internalInstance: VisibilityProvider;
@@ -38,7 +39,7 @@ export class VisibilityProvider {
3839
await VoipUserMapper.sharedInstance().onNewInvitedRoom(room);
3940
}
4041

41-
public isRoomVisible(room?: Room): boolean {
42+
public async isRoomVisible(room?: Room): Promise<boolean> {
4243
if (!room) {
4344
return false;
4445
}
@@ -50,6 +51,21 @@ export class VisibilityProvider {
5051
return false;
5152
}
5253

54+
if (room.getMyMembership() === "invite") {
55+
// Find out whether the invite should be hidden.
56+
const cli = MatrixClientPeg.get();
57+
const myUserId = cli.getUserId();
58+
const inviter = room.currentState.getMember(myUserId);
59+
if (inviter?.events?.member) {
60+
const inviterUserId = inviter.events.member.getSender();
61+
const rule = await cli.ignoredInvites.getRuleForInvite({ roomId: room.roomId, sender: inviterUserId });
62+
if (rule) {
63+
// Indeed, there is a rule that specifies we should hide the invite.
64+
return false;
65+
}
66+
}
67+
}
68+
5369
// hide space rooms as they'll be shown in the SpacePanel
5470
if (room.isSpaceRoom()) {
5571
return false;

test/stores/room-list/algorithms/Algorithm-test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,14 @@ describe("Algorithm", () => {
3838
let client: MockedObject<MatrixClient>;
3939
let algorithm: Algorithm;
4040

41-
beforeEach(() => {
41+
beforeEach(async () => {
4242
stubClient();
4343
client = mocked(MatrixClientPeg.get());
4444
DMRoomMap.makeShared();
4545

4646
algorithm = new Algorithm();
4747
algorithm.start();
48-
algorithm.populateTags(
48+
await algorithm.populateTags(
4949
{ [DefaultTagID.Untagged]: SortAlgorithm.Alphabetic },
5050
{ [DefaultTagID.Untagged]: ListAlgorithm.Natural },
5151
);
@@ -75,7 +75,7 @@ describe("Algorithm", () => {
7575
client.reEmitter.reEmit(roomWithCall, [RoomStateEvent.Events]);
7676

7777
for (const room of client.getRooms()) jest.spyOn(room, "getMyMembership").mockReturnValue("join");
78-
algorithm.setKnownRooms(client.getRooms());
78+
await algorithm.setKnownRooms(client.getRooms());
7979

8080
setupAsyncStoreWithClient(CallStore.instance, client);
8181
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);

test/stores/room-list/filters/VisibilityProvider-test.ts

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import LegacyCallHandler from "../../../../src/LegacyCallHandler";
2222
import VoipUserMapper from "../../../../src/VoipUserMapper";
2323
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../../../src/models/LocalRoom";
2424
import { RoomListCustomisations } from "../../../../src/customisations/RoomList";
25-
import { createTestClient } from "../../../test-utils";
25+
import { createTestClient, IGNORE_INVITES_FROM_THIS_USER, IGNORE_INVITES_TO_THIS_ROOM, stubClient }
26+
from "../../../test-utils";
27+
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
2628

2729
jest.mock("../../../../src/VoipUserMapper", () => ({
2830
sharedInstance: jest.fn(),
@@ -40,9 +42,29 @@ jest.mock("../../../../src/customisations/RoomList", () => ({
4042
},
4143
}));
4244

43-
const createRoom = (isSpaceRoom = false): Room => {
45+
const createRoom = ({ isSpaceRoom, inviter, roomId }: { isSpaceRoom?: boolean, inviter?: string, roomId?: string } =
46+
{ isSpaceRoom: false, roomId: `${Math.random()}:example.org` }): Room => {
4447
return {
48+
roomId,
4549
isSpaceRoom: () => isSpaceRoom,
50+
getMyMembership: () =>
51+
inviter ? "invite" : "join",
52+
currentState: {
53+
getMember(userId: string): any | null {
54+
if (userId != MatrixClientPeg.get().getUserId()) {
55+
return null;
56+
}
57+
return {
58+
events: {
59+
member: {
60+
getSender() {
61+
return inviter;
62+
},
63+
},
64+
},
65+
};
66+
},
67+
},
4668
} as unknown as Room;
4769
};
4870

@@ -61,6 +83,7 @@ describe("VisibilityProvider", () => {
6183
isVirtualRoom: jest.fn(),
6284
} as unknown as VoipUserMapper;
6385
mocked(VoipUserMapper.sharedInstance).mockReturnValue(mockVoipUserMapper);
86+
stubClient();
6487
});
6588

6689
describe("instance", () => {
@@ -86,39 +109,58 @@ describe("VisibilityProvider", () => {
86109
mocked(mockVoipUserMapper.isVirtualRoom).mockReturnValue(true);
87110
});
88111

89-
it("should return return false", () => {
112+
it("should return return false", async () => {
90113
const room = createRoom();
91-
expect(VisibilityProvider.instance.isRoomVisible(room)).toBe(false);
114+
expect(await VisibilityProvider.instance.isRoomVisible(room)).toBe(false);
92115
expect(mockVoipUserMapper.isVirtualRoom).toHaveBeenCalledWith(room);
93116
});
94117
});
95118

96-
it("should return false without room", () => {
97-
expect(VisibilityProvider.instance.isRoomVisible()).toBe(false);
119+
it("should return false without room", async () => {
120+
expect(await VisibilityProvider.instance.isRoomVisible()).toBe(false);
98121
});
99122

100-
it("should return false for a space room", () => {
101-
const room = createRoom(true);
102-
expect(VisibilityProvider.instance.isRoomVisible(room)).toBe(false);
123+
it("should return false for a space room", async () => {
124+
const room = createRoom({ isSpaceRoom: true });
125+
expect(await VisibilityProvider.instance.isRoomVisible(room)).toBe(false);
103126
});
104127

105-
it("should return false for a local room", () => {
128+
it("should return false for a local room", async () => {
106129
const room = createLocalRoom();
107-
expect(VisibilityProvider.instance.isRoomVisible(room)).toBe(false);
130+
expect(await VisibilityProvider.instance.isRoomVisible(room)).toBe(false);
108131
});
109132

110-
it("should return false if visibility customisation returns false", () => {
133+
it("should return false if visibility customisation returns false", async () => {
111134
mocked(RoomListCustomisations.isRoomVisible).mockReturnValue(false);
112135
const room = createRoom();
113-
expect(VisibilityProvider.instance.isRoomVisible(room)).toBe(false);
136+
expect(await VisibilityProvider.instance.isRoomVisible(room)).toBe(false);
114137
expect(RoomListCustomisations.isRoomVisible).toHaveBeenCalledWith(room);
115138
});
116139

117-
it("should return true if visibility customisation returns true", () => {
140+
it("should return true if visibility customisation returns true", async () => {
118141
mocked(RoomListCustomisations.isRoomVisible).mockReturnValue(true);
119142
const room = createRoom();
120-
expect(VisibilityProvider.instance.isRoomVisible(room)).toBe(true);
143+
expect(await VisibilityProvider.instance.isRoomVisible(room)).toBe(true);
121144
expect(RoomListCustomisations.isRoomVisible).toHaveBeenCalledWith(room);
122145
});
146+
147+
it("should return true if the room is an invite but hasn't been marked as ignored", async () => {
148+
mocked(RoomListCustomisations.isRoomVisible).mockReturnValue(true);
149+
const room = createRoom({ inviter: "@good-user:example.org" });
150+
expect(await VisibilityProvider.instance.isRoomVisible(room)).toBe(true);
151+
expect(RoomListCustomisations.isRoomVisible).toHaveBeenCalledWith(room);
152+
});
153+
154+
it("should return false if the room is an invite and the sender has been marked as ignored", async () => {
155+
mocked(RoomListCustomisations.isRoomVisible).mockReturnValue(true);
156+
const room = createRoom({ inviter: IGNORE_INVITES_FROM_THIS_USER });
157+
expect(await VisibilityProvider.instance.isRoomVisible(room)).toBe(false);
158+
});
159+
160+
it("should return false if the room is an invite and the roomId has been marked as ignored", async () => {
161+
mocked(RoomListCustomisations.isRoomVisible).mockReturnValue(true);
162+
const room = createRoom({ inviter: "@good-user:example.org", roomId: IGNORE_INVITES_TO_THIS_ROOM });
163+
expect(await VisibilityProvider.instance.isRoomVisible(room)).toBe(false);
164+
});
123165
});
124166
});

test/test-utils/test-utils.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ export function stubClient() {
6666
MatrixClientBackedSettingsHandler.matrixClient = client;
6767
}
6868

69+
export const IGNORE_INVITES_TO_THIS_ROOM = "$ignore-invites-to-this-room:example.org";
70+
export const IGNORE_INVITES_TO_THIS_ROOM_ISSUER = "@user-who-decided-to-ignore-invites-to-this-room:example.org";
71+
export const IGNORE_INVITES_FROM_THIS_USER = "@ignore-invites-from-this-sender:example.org";
72+
export const IGNORE_INVITES_FROM_THIS_USER_ISSUER = "@user-who-decided-to-ignore-invites-from-this-user:example.org";
73+
6974
/**
7075
* Create a stubbed-out MatrixClient
7176
*
@@ -174,6 +179,21 @@ export function createTestClient(): MatrixClient {
174179
sendToDevice: jest.fn().mockResolvedValue(undefined),
175180
queueToDevice: jest.fn().mockResolvedValue(undefined),
176181
encryptAndSendToDevices: jest.fn().mockResolvedValue(undefined),
182+
ignoredInvites: {
183+
getRuleForInvite: jest.fn().mockImplementation(({ roomId, sender }) => {
184+
if (roomId === IGNORE_INVITES_TO_THIS_ROOM) {
185+
return Promise.resolve(new MatrixEvent({
186+
sender: IGNORE_INVITES_TO_THIS_ROOM_ISSUER,
187+
}));
188+
}
189+
if (sender === IGNORE_INVITES_FROM_THIS_USER) {
190+
return Promise.resolve(new MatrixEvent({
191+
sender: IGNORE_INVITES_FROM_THIS_USER_ISSUER,
192+
}));
193+
}
194+
return Promise.resolve(null);
195+
}),
196+
},
177197
} as unknown as MatrixClient;
178198

179199
client.reEmitter = new ReEmitter(client);

0 commit comments

Comments
 (0)