Skip to content

Commit fd0c4a7

Browse files
authored
Ensure we don't overinflate the total notification count (#3634)
* Ensure we don't overinflate the total notification count By correctly comparing push rules before & after decryption * DRY the code * Testsssss * Update tests
1 parent 615f7f9 commit fd0c4a7

File tree

6 files changed

+81
-58
lines changed

6 files changed

+81
-58
lines changed

spec/unit/event-timeline-set.spec.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,13 @@ describe("EventTimelineSet", () => {
303303
messageEventIsDecryptionFailureSpy.mockReturnValue(true);
304304
replyEventIsDecryptionFailureSpy.mockReturnValue(true);
305305

306-
messageEvent.emit(MatrixEventEvent.Decrypted, messageEvent);
307-
replyEvent.emit(MatrixEventEvent.Decrypted, replyEvent);
306+
messageEvent.emit(
307+
MatrixEventEvent.Decrypted,
308+
messageEvent,
309+
undefined,
310+
messageEvent.getPushDetails(),
311+
);
312+
replyEvent.emit(MatrixEventEvent.Decrypted, replyEvent, undefined, replyEvent.getPushDetails());
308313

309314
// simulate decryption
310315
messageEventIsDecryptionFailureSpy.mockReturnValue(false);
@@ -313,8 +318,13 @@ describe("EventTimelineSet", () => {
313318
messageEventShouldAttemptDecryptionSpy.mockReturnValue(false);
314319
replyEventShouldAttemptDecryptionSpy.mockReturnValue(false);
315320

316-
messageEvent.emit(MatrixEventEvent.Decrypted, messageEvent);
317-
replyEvent.emit(MatrixEventEvent.Decrypted, replyEvent);
321+
messageEvent.emit(
322+
MatrixEventEvent.Decrypted,
323+
messageEvent,
324+
undefined,
325+
messageEvent.getPushDetails(),
326+
);
327+
replyEvent.emit(MatrixEventEvent.Decrypted, replyEvent, undefined, replyEvent.getPushDetails());
318328
});
319329

320330
itShouldReturnTheRelatedEvents();

spec/unit/notifications.spec.ts

+21-10
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ describe("fixNotificationCountOnDecryption", () => {
5454
mockClient = getMockClientWithEventEmitter({
5555
...mockClientMethodsUser(),
5656
isInitialSyncComplete: jest.fn().mockReturnValue(false),
57-
getPushActionsForEvent: jest.fn().mockReturnValue(mkPushAction(true, true)),
57+
getPushActionsForEvent: jest.fn((ev) => event.getPushActions() ?? mkPushAction(true, true)),
5858
getRoom: jest.fn().mockImplementation(() => room),
5959
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
6060
supportsThreads: jest.fn().mockReturnValue(true),
@@ -125,15 +125,15 @@ describe("fixNotificationCountOnDecryption", () => {
125125
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 1);
126126
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
127127

128-
event.getPushActions = jest.fn().mockReturnValue(mkPushAction(false, false));
129-
threadEvent.getPushActions = jest.fn().mockReturnValue(mkPushAction(false, false));
128+
event.getPushActions = jest.fn().mockReturnValue(mkPushAction(true, true));
129+
threadEvent.getPushActions = jest.fn().mockReturnValue(mkPushAction(true, true));
130130
});
131131

132132
it("changes the room count to highlight on decryption", () => {
133133
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(2);
134134
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0);
135135

136-
fixNotificationCountOnDecryption(mockClient, event);
136+
fixNotificationCountOnDecryption(mockClient, event, {});
137137

138138
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(3);
139139
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
@@ -143,7 +143,7 @@ describe("fixNotificationCountOnDecryption", () => {
143143
room.setUnreadNotificationCount(NotificationCountType.Total, 0);
144144
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
145145

146-
fixNotificationCountOnDecryption(mockClient, event);
146+
fixNotificationCountOnDecryption(mockClient, event, {});
147147

148148
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(1);
149149
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
@@ -153,7 +153,7 @@ describe("fixNotificationCountOnDecryption", () => {
153153
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1);
154154
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
155155

156-
fixNotificationCountOnDecryption(mockClient, threadEvent);
156+
fixNotificationCountOnDecryption(mockClient, threadEvent, {});
157157

158158
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(2);
159159
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(1);
@@ -163,7 +163,7 @@ describe("fixNotificationCountOnDecryption", () => {
163163
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
164164
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
165165

166-
fixNotificationCountOnDecryption(mockClient, event);
166+
fixNotificationCountOnDecryption(mockClient, event, {});
167167

168168
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0);
169169
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
@@ -187,7 +187,7 @@ describe("fixNotificationCountOnDecryption", () => {
187187
event: true,
188188
});
189189

190-
fixNotificationCountOnDecryption(mockClient, unknownThreadEvent);
190+
fixNotificationCountOnDecryption(mockClient, unknownThreadEvent, {});
191191

192192
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0);
193193
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
@@ -201,7 +201,7 @@ describe("fixNotificationCountOnDecryption", () => {
201201
event.getPushActions = jest.fn().mockReturnValue(mkPushAction(true, false));
202202
mockClient.getPushActionsForEvent = jest.fn().mockReturnValue(mkPushAction(false, false));
203203

204-
fixNotificationCountOnDecryption(mockClient, event);
204+
fixNotificationCountOnDecryption(mockClient, event, {});
205205
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
206206
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0);
207207
});
@@ -213,7 +213,7 @@ describe("fixNotificationCountOnDecryption", () => {
213213
threadEvent.getPushActions = jest.fn().mockReturnValue(mkPushAction(true, false));
214214
mockClient.getPushActionsForEvent = jest.fn().mockReturnValue(mkPushAction(false, false));
215215

216-
fixNotificationCountOnDecryption(mockClient, event);
216+
fixNotificationCountOnDecryption(mockClient, event, {});
217217
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0);
218218
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
219219
});
@@ -231,4 +231,15 @@ describe("fixNotificationCountOnDecryption", () => {
231231
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 5);
232232
expect(cb).toHaveBeenLastCalledWith({ highlight: 5 }, "$123");
233233
});
234+
235+
it("should not increment notification count where event was already contributing notification", () => {
236+
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(2);
237+
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0);
238+
239+
event.getPushActions = jest.fn().mockReturnValue(mkPushAction(true, false));
240+
fixNotificationCountOnDecryption(mockClient, event, { actions: { notify: true, tweaks: {} } });
241+
242+
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(2);
243+
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0);
244+
});
234245
});

spec/unit/room-state.spec.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1037,7 +1037,12 @@ describe("RoomState", function () {
10371037

10381038
// this event is a message after decryption
10391039
decryptingRelatedEvent.event.type = EventType.RoomMessage;
1040-
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted, decryptingRelatedEvent);
1040+
decryptingRelatedEvent.emit(
1041+
MatrixEventEvent.Decrypted,
1042+
decryptingRelatedEvent,
1043+
undefined,
1044+
decryptingRelatedEvent.getPushDetails(),
1045+
);
10411046

10421047
expect(addLocationsSpy).not.toHaveBeenCalled();
10431048
});

spec/unit/room.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3428,13 +3428,13 @@ describe("Room", function () {
34283428
expect(room.polls.get(pollStartEventId)).toBeUndefined();
34293429

34303430
// now emit a Decrypted event but keep the decryption failure
3431-
pollStartEvent.emit(MatrixEventEvent.Decrypted, pollStartEvent);
3431+
pollStartEvent.emit(MatrixEventEvent.Decrypted, pollStartEvent, undefined, pollStartEvent.getPushDetails());
34323432
// still do not expect a poll to show up for the room
34333433
expect(room.polls.get(pollStartEventId)).toBeUndefined();
34343434

34353435
// clear decryption failure and emit a Decrypted event again
34363436
isDecryptionFailureSpy.mockRestore();
3437-
pollStartEvent.emit(MatrixEventEvent.Decrypted, pollStartEvent);
3437+
pollStartEvent.emit(MatrixEventEvent.Decrypted, pollStartEvent, undefined, pollStartEvent.getPushDetails());
34383438

34393439
// the poll should now show up in the room's polls
34403440
const poll = room.polls.get(pollStartEventId);

src/client.ts

+34-39
Original file line numberDiff line numberDiff line change
@@ -1371,8 +1371,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
13711371
// actions for themselves, so we have to kinda help them out when they are encrypted.
13721372
// We do this so that push rules are correctly executed on events in their decrypted
13731373
// state, such as highlights when the user's name is mentioned.
1374-
this.on(MatrixEventEvent.Decrypted, (event) => {
1375-
fixNotificationCountOnDecryption(this, event);
1374+
this.on(MatrixEventEvent.Decrypted, (event, _, pushDetails) => {
1375+
fixNotificationCountOnDecryption(this, event, pushDetails);
13761376
});
13771377

13781378
// Like above, we have to listen for read receipts from ourselves in order to
@@ -9851,26 +9851,23 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
98519851
* Servers do not have enough knowledge about encrypted events to calculate an
98529852
* accurate notification_count
98539853
*/
9854-
export function fixNotificationCountOnDecryption(cli: MatrixClient, event: MatrixEvent): void {
9854+
export function fixNotificationCountOnDecryption(
9855+
cli: MatrixClient,
9856+
event: MatrixEvent,
9857+
pushDetails: PushDetails,
9858+
): void {
98559859
const ourUserId = cli.getUserId();
98569860
const eventId = event.getId();
98579861

98589862
const room = cli.getRoom(event.getRoomId());
98599863
if (!room || !ourUserId || !eventId) return;
98609864

9861-
const oldActions = event.getPushActions();
9865+
// We cannot call event.getPushActions here as the decryption loop just nulled them for re-calculation
9866+
const oldActions = pushDetails.actions;
98629867
const actions = cli.getPushActionsForEvent(event, true);
98639868

98649869
const isThreadEvent = !!event.threadRootId && !event.isThreadRoot;
98659870

9866-
const currentHighlightCount = room.getUnreadCountForEventContext(NotificationCountType.Highlight, event);
9867-
9868-
// Ensure the unread counts are kept up to date if the event is encrypted
9869-
// We also want to make sure that the notification count goes up if we already
9870-
// have encrypted events to avoid other code from resetting 'highlight' to zero.
9871-
const oldHighlight = !!oldActions?.tweaks?.highlight;
9872-
const newHighlight = !!actions?.tweaks?.highlight;
9873-
98749871
let hasReadEvent;
98759872
if (isThreadEvent) {
98769873
const thread = room.getThread(event.threadRootId);
@@ -9892,37 +9889,35 @@ export function fixNotificationCountOnDecryption(cli: MatrixClient, event: Matri
98929889
return;
98939890
}
98949891

9895-
if (oldHighlight !== newHighlight || currentHighlightCount > 0) {
9896-
// TODO: Handle mentions received while the client is offline
9897-
// See also https://github.com/vector-im/element-web/issues/9069
9898-
let newCount = currentHighlightCount;
9899-
if (newHighlight && !oldHighlight) newCount++;
9900-
if (!newHighlight && oldHighlight) newCount--;
9901-
9902-
if (isThreadEvent) {
9903-
room.setThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Highlight, newCount);
9892+
// Ensure the unread counts are kept up to date if the event is encrypted
9893+
// We also want to make sure that the notification count goes up if we already
9894+
// have encrypted events to avoid other code from resetting 'highlight' to zero.
9895+
// TODO: Handle mentions received while the client is offline
9896+
// See also https://github.com/vector-im/element-web/issues/9069
9897+
for (const type of [NotificationCountType.Highlight, NotificationCountType.Total]) {
9898+
let count = room.getUnreadCountForEventContext(type, event);
9899+
9900+
let oldValue: boolean;
9901+
let newValue: boolean;
9902+
if (type === NotificationCountType.Total) {
9903+
// Total count is used to typically increment a room notification counter, but not loudly highlight it.
9904+
// `notify` is used in practice for incrementing the total count
9905+
oldValue = !!oldActions?.notify;
9906+
newValue = !!actions?.notify;
99049907
} else {
9905-
room.setUnreadNotificationCount(NotificationCountType.Highlight, newCount);
9908+
oldValue = !!oldActions?.tweaks?.highlight;
9909+
newValue = !!actions?.tweaks?.highlight;
99069910
}
9907-
}
99089911

9909-
// Total count is used to typically increment a room notification counter, but not loudly highlight it.
9910-
const currentTotalCount = room.getUnreadCountForEventContext(NotificationCountType.Total, event);
9912+
if (oldValue !== newValue || count > 0) {
9913+
if (newValue && !oldValue) count++;
9914+
if (!newValue && oldValue) count--;
99119915

9912-
// `notify` is used in practice for incrementing the total count
9913-
const newNotify = !!actions?.notify;
9914-
9915-
// The room total count is NEVER incremented by the server for encrypted rooms. We basically ignore
9916-
// the server here as it's always going to tell us to increment for encrypted events.
9917-
if (newNotify) {
9918-
if (isThreadEvent) {
9919-
room.setThreadUnreadNotificationCount(
9920-
event.threadRootId,
9921-
NotificationCountType.Total,
9922-
currentTotalCount + 1,
9923-
);
9924-
} else {
9925-
room.setUnreadNotificationCount(NotificationCountType.Total, currentTotalCount + 1);
9916+
if (isThreadEvent) {
9917+
room.setThreadUnreadNotificationCount(event.threadRootId, type, count);
9918+
} else {
9919+
room.setUnreadNotificationCount(type, count);
9920+
}
99269921
}
99279922
}
99289923
}

src/models/event.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ export type MatrixEventHandlerMap = {
226226
* @param event - The matrix event which has been decrypted
227227
* @param err - The error that occurred during decryption, or `undefined` if no error occurred.
228228
*/
229-
[MatrixEventEvent.Decrypted]: (event: MatrixEvent, err?: Error) => void;
229+
[MatrixEventEvent.Decrypted]: (event: MatrixEvent, err: Error | undefined, pushDetails: PushDetails) => void;
230230
[MatrixEventEvent.BeforeRedaction]: (event: MatrixEvent, redactionEvent: MatrixEvent) => void;
231231
[MatrixEventEvent.VisibilityChange]: (event: MatrixEvent, visible: boolean) => void;
232232
[MatrixEventEvent.LocalEventIdReplaced]: (event: MatrixEvent) => void;
@@ -916,6 +916,8 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
916916
this.retryDecryption = false;
917917
this.setClearData(res);
918918

919+
const pushDetails = this.getPushDetails();
920+
919921
// Before we emit the event, clear the push actions so that they can be recalculated
920922
// by relevant code. We do this because the clear event has now changed, making it
921923
// so that existing rules can be re-run over the applicable properties. Stuff like
@@ -925,7 +927,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
925927
this.setPushDetails();
926928

927929
if (options.emit !== false) {
928-
this.emit(MatrixEventEvent.Decrypted, this, err);
930+
this.emit(MatrixEventEvent.Decrypted, this, err, pushDetails);
929931
}
930932

931933
return;

0 commit comments

Comments
 (0)