From 389cc12214ef9a51acbea1cd5171a69942771ddd Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 3 Oct 2022 16:37:44 +0100 Subject: [PATCH 01/13] Add support for unread thread notifications --- src/@types/sync.ts | 26 ++++++++++++++++++++++++++ src/filter.ts | 13 ++++++++++++- src/models/room.ts | 32 +++++++++++++++++++++++++++++++- src/sync-accumulator.ts | 16 +++++++++++++--- src/sync.ts | 26 ++++++++++++++++++++++++++ 5 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 src/@types/sync.ts diff --git a/src/@types/sync.ts b/src/@types/sync.ts new file mode 100644 index 00000000000..f25bbf2e497 --- /dev/null +++ b/src/@types/sync.ts @@ -0,0 +1,26 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { UnstableValue } from "matrix-events-sdk/lib/NamespacedValue"; + +/** + * https://github.com/matrix-org/matrix-doc/pull/3773 + * + * @experimental + */ +export const UNREAD_THREAD_NOTIFICATIONS = new UnstableValue( + "unread_thread_notifications", + "org.matrix.msc3773.unread_thread_notifications"); diff --git a/src/filter.ts b/src/filter.ts index 663ba1bb932..48a4cbf15fd 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -22,6 +22,7 @@ import { EventType, RelationType, } from "./@types/event"; +import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync"; import { FilterComponent, IFilterComponent } from "./filter-component"; import { MatrixEvent } from "./models/event"; @@ -57,6 +58,8 @@ export interface IRoomEventFilter extends IFilterComponent { types?: Array; related_by_senders?: Array; related_by_rel_types?: string[]; + unread_thread_notifications?: boolean; + "org.matrix.msc3773.unread_thread_notifications"?: boolean; // Unstable values "io.element.relation_senders"?: Array; @@ -220,7 +223,15 @@ export class Filter { setProp(this.definition, "room.timeline.limit", limit); } - setLazyLoadMembers(enabled: boolean) { + /** + * Enable threads unread notification + * @param {boolean} enabled + */ + public setUnreadThreadNotifications(enabled: boolean): void { + setProp(this.definition, `room.state.${UNREAD_THREAD_NOTIFICATIONS.name}`, !!enabled); + } + + setLazyLoadMembers(enabled: boolean): void { setProp(this.definition, "room.state.lazy_load_members", !!enabled); } diff --git a/src/models/room.ts b/src/models/room.ts index 75267fa2a29..c8c9d2a990a 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -96,6 +96,8 @@ export interface IRecommendedVersion { // price to pay to keep matrix-js-sdk responsive. const MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH = 30; +type NotificationCount = Partial>; + export enum NotificationCountType { Highlight = "highlight", Total = "total", @@ -183,7 +185,8 @@ export type RoomEventHandlerMap = { export class Room extends ReadReceipt { public readonly reEmitter: TypedReEmitter; private txnToEvent: Record = {}; // Pending in-flight requests { string: MatrixEvent } - private notificationCounts: Partial> = {}; + private notificationCounts: NotificationCount = {}; + public threadNotifications: Record = {}; private readonly timelineSets: EventTimelineSet[]; public readonly threadsTimelineSets: EventTimelineSet[] = []; // any filtered timeline sets we're maintaining for this room @@ -1180,6 +1183,33 @@ export class Room extends ReadReceipt { return this.notificationCounts[type]; } + /** + * Get one of the notification counts for a thread + * @param threadId the root event ID + * @param type The type of notification count to get. default: 'total' + * @returns The notification count, or undefined if there is no count + * for this type. + */ + public getThreadUnreadNotificationCount(threadId: string, type = NotificationCountType.Total): number | undefined { + return this.threadNotifications[threadId]?.[type]; + } + + /** + * Swet one of the notification count for a thread + * @param threadId the root event ID + * @param type The type of notification count to get. default: 'total' + * @returns {void} + */ + public setThreadUnreadNotificationCount(threadId: string, type: NotificationCountType, count: number): void { + this.threadNotifications[threadId] = { + highlight: this.threadNotifications[threadId]?.highlight ?? 0, + total: this.threadNotifications[threadId]?.total ?? 0, + ...{ + [type]: count, + }, + }; + } + /** * Set one of the notification counts for this room * @param {String} type The type of notification count to set. diff --git a/src/sync-accumulator.ts b/src/sync-accumulator.ts index 037c9231b21..ec60c1c3a73 100644 --- a/src/sync-accumulator.ts +++ b/src/sync-accumulator.ts @@ -25,6 +25,7 @@ import { IContent, IUnsigned } from "./models/event"; import { IRoomSummary } from "./models/room-summary"; import { EventType } from "./@types/event"; import { ReceiptType } from "./@types/read_receipts"; +import { UNREAD_THREAD_NOTIFICATIONS } from './@types/sync'; interface IOpts { maxTimelineEntries?: number; @@ -41,7 +42,7 @@ export interface IEphemeral { } /* eslint-disable camelcase */ -interface IUnreadNotificationCounts { +interface UnreadNotificationCounts { highlight_count?: number; notification_count?: number; } @@ -75,7 +76,9 @@ export interface IJoinedRoom { timeline: ITimeline; ephemeral: IEphemeral; account_data: IAccountData; - unread_notifications: IUnreadNotificationCounts; + unread_notifications: UnreadNotificationCounts; + unread_thread_notifications?: Record; + "org.matrix.msc3773.unread_thread_notifications"?: Record; } export interface IStrippedState { @@ -153,7 +156,8 @@ interface IRoom { }[]; _summary: Partial; _accountData: { [eventType: string]: IMinimalEvent }; - _unreadNotifications: Partial; + _unreadNotifications: Partial; + _unreadThreadNotifications?: Record>; _readReceipts: { [userId: string]: { data: IMinimalEvent; @@ -362,6 +366,7 @@ export class SyncAccumulator { _timeline: [], _accountData: Object.create(null), _unreadNotifications: {}, + _unreadThreadNotifications: {}, _summary: {}, _readReceipts: {}, }; @@ -379,6 +384,10 @@ export class SyncAccumulator { if (data.unread_notifications) { currentData._unreadNotifications = data.unread_notifications; } + currentData._unreadThreadNotifications = data[UNREAD_THREAD_NOTIFICATIONS.stable] + ?? data[UNREAD_THREAD_NOTIFICATIONS.unstable] + ?? undefined; + if (data.summary) { const HEROES_KEY = "m.heroes"; const INVITED_COUNT_KEY = "m.invited_member_count"; @@ -537,6 +546,7 @@ export class SyncAccumulator { prev_batch: null, }, unread_notifications: roomData._unreadNotifications, + unread_thread_notifications: roomData._unreadThreadNotifications, summary: roomData._summary as IRoomSummary, }; // Add account data diff --git a/src/sync.ts b/src/sync.ts index 7685479499a..d1866bf2a92 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -58,6 +58,7 @@ import { RoomMemberEvent } from "./models/room-member"; import { BeaconEvent } from "./models/beacon"; import { IEventsResponse } from "./@types/requests"; import { IAbortablePromise } from "./@types/partials"; +import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync"; const DEBUG = true; @@ -705,6 +706,9 @@ export class SyncApi { const initialFilter = this.buildDefaultFilter(); initialFilter.setDefinition(filter.getDefinition()); initialFilter.setTimelineLimit(this.opts.initialSyncLimit); + const supportsThreadNotifications = + await this.client.doesServerSupportUnstableFeature("org.matrix.msc3773"); + initialFilter.setUnreadThreadNotifications(supportsThreadNotifications); // Use an inline filter, no point uploading it for a single usage firstSyncFilter = JSON.stringify(initialFilter.getDefinition()); } @@ -1264,6 +1268,28 @@ export class SyncApi { } } + room.threadNotifications = {}; + const unreadThreadNotifications = joinObj[UNREAD_THREAD_NOTIFICATIONS.name]; + if (unreadThreadNotifications) { + Object.entries(unreadThreadNotifications).forEach(([threadId, unreadNotification]) => { + room.setThreadUnreadNotificationCount( + threadId, + NotificationCountType.Total, + unreadNotification.notification_count, + ); + + const hasUnreadNotification = + room.getThreadUnreadNotificationCount(threadId, NotificationCountType.Highlight) <= 0; + if (!encrypted || (encrypted && hasUnreadNotification)) { + room.setThreadUnreadNotificationCount( + threadId, + NotificationCountType.Highlight, + unreadNotification.highlight_count, + ); + } + }); + } + joinObj.timeline = joinObj.timeline || {} as ITimeline; if (joinObj.isBrandNewRoom) { From b1a08c0c716d370de774e1f97845db2c62c15842 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 3 Oct 2022 16:51:32 +0100 Subject: [PATCH 02/13] Add support for encrypted events in unread thread notifications --- src/client.ts | 34 ++------------------------- src/utils.ts | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 32 deletions(-) diff --git a/src/client.ts b/src/client.ts index e0e05112627..fc71af9baf0 100644 --- a/src/client.ts +++ b/src/client.ts @@ -36,7 +36,7 @@ import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall, suppor import { Filter, IFilterDefinition } from "./filter"; import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler'; import * as utils from './utils'; -import { sleep } from './utils'; +import { fixNotificationCountOnDecryption, sleep } from './utils'; import { Direction, EventTimeline } from "./models/event-timeline"; import { IActionsObject, PushProcessor } from "./pushprocessor"; import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery"; @@ -1080,37 +1080,7 @@ export class MatrixClient extends TypedEventEmitter { - const oldActions = event.getPushActions(); - const actions = this.getPushActionsForEvent(event, true); - - const room = this.getRoom(event.getRoomId()); - if (!room) return; - - const currentCount = room.getUnreadNotificationCount(NotificationCountType.Highlight); - - // Ensure the unread counts are kept up to date if the event is encrypted - // We also want to make sure that the notification count goes up if we already - // have encrypted events to avoid other code from resetting 'highlight' to zero. - const oldHighlight = !!oldActions?.tweaks?.highlight; - const newHighlight = !!actions?.tweaks?.highlight; - if (oldHighlight !== newHighlight || currentCount > 0) { - // TODO: Handle mentions received while the client is offline - // See also https://github.com/vector-im/element-web/issues/9069 - if (!room.hasUserReadEvent(this.getUserId(), event.getId())) { - let newCount = currentCount; - if (newHighlight && !oldHighlight) newCount++; - if (!newHighlight && oldHighlight) newCount--; - room.setUnreadNotificationCount(NotificationCountType.Highlight, newCount); - - // Fix 'Mentions Only' rooms from not having the right badge count - const totalCount = room.getUnreadNotificationCount(NotificationCountType.Total); - if (totalCount < newCount) { - room.setUnreadNotificationCount(NotificationCountType.Total, newCount); - } - } - } - }); + this.on(MatrixEventEvent.Decrypted, (event) => fixNotificationCountOnDecryption(this, event)); // Like above, we have to listen for read receipts from ourselves in order to // correctly handle notification counts on encrypted rooms. diff --git a/src/utils.ts b/src/utils.ts index 2875cf3cfb0..6254626eabd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -27,6 +27,8 @@ import type * as NodeCrypto from "crypto"; import { MatrixEvent } from "./models/event"; import { M_TIMESTAMP } from "./@types/location"; import { ReceiptType } from "./@types/read_receipts"; +import { MatrixClient } from "./client"; +import { NotificationCountType } from "./models/room"; const interns = new Map(); @@ -674,3 +676,66 @@ export function isSupportedReceiptType(receiptType: string): boolean { return [ReceiptType.Read, ReceiptType.ReadPrivate].includes(receiptType as ReceiptType); } +/** + * recalculates an accurate notifications count on event decryption. + * Servers do not have enough knowledge about encrypted events to calculate an + * accurate notification_count + */ +export function fixNotificationCountOnDecryption(cli: MatrixClient, event: MatrixEvent): void { + const oldActions = event.getPushActions(); + const actions = cli.getPushActionsForEvent(event, true); + + const room = cli.getRoom(event.getRoomId()); + if (!room) return; + + const isThreadEvent = !!event.threadRootId; + const currentCount = isThreadEvent + ? room.getThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Highlight) + : room.getUnreadNotificationCount(NotificationCountType.Highlight); + + // Ensure the unread counts are kept up to date if the event is encrypted + // We also want to make sure that the notification count goes up if we already + // have encrypted events to avoid other code from resetting 'highlight' to zero. + const oldHighlight = !!oldActions?.tweaks?.highlight; + const newHighlight = !!actions?.tweaks?.highlight; + if (oldHighlight !== newHighlight || currentCount > 0) { + // TODO: Handle mentions received while the client is offline + // See also https://github.com/vector-im/element-web/issues/9069 + const hasReadEvent = isThreadEvent + ? room.getThread(event.threadRootId).hasUserReadEvent(cli.getUserId(), event.getId()) + : room.hasUserReadEvent(cli.getUserId(), event.getId()); + + if (!hasReadEvent) { + let newCount = currentCount; + if (newHighlight && !oldHighlight) newCount++; + if (!newHighlight && oldHighlight) newCount--; + + if (isThreadEvent) { + room.setThreadUnreadNotificationCount( + event.threadRootId, + NotificationCountType.Highlight, + newCount, + ); + } else { + room.setUnreadNotificationCount(NotificationCountType.Highlight, newCount); + } + + // Fix 'Mentions Only' rooms from not having the right badge count + const totalCount = isThreadEvent + ? room.getThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Total) + : room.getUnreadNotificationCount(NotificationCountType.Total); + + if (totalCount < newCount) { + if (isThreadEvent) { + room.setThreadUnreadNotificationCount( + event.threadRootId, + NotificationCountType.Total, + newCount, + ); + } else { + room.setUnreadNotificationCount(NotificationCountType.Total, newCount); + } + } + } + } +} From 15d269d079f3add9b024bedb29174f474d4a0a55 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 3 Oct 2022 16:57:59 +0100 Subject: [PATCH 03/13] Fix unread_thread_notifications filter definition --- src/filter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/filter.ts b/src/filter.ts index 48a4cbf15fd..496e18ece99 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -228,7 +228,7 @@ export class Filter { * @param {boolean} enabled */ public setUnreadThreadNotifications(enabled: boolean): void { - setProp(this.definition, `room.state.${UNREAD_THREAD_NOTIFICATIONS.name}`, !!enabled); + setProp(this.definition, `room.timeline.${UNREAD_THREAD_NOTIFICATIONS.name}`, !!enabled); } setLazyLoadMembers(enabled: boolean): void { From 1dcd56c73906e71111f6919bd2800154adcab34b Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 3 Oct 2022 17:36:01 +0100 Subject: [PATCH 04/13] Fix circular dependencies import --- src/client.ts | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/utils.ts | 66 ------------------------------------------------ 2 files changed, 68 insertions(+), 67 deletions(-) diff --git a/src/client.ts b/src/client.ts index fc71af9baf0..6f724494f35 100644 --- a/src/client.ts +++ b/src/client.ts @@ -36,7 +36,7 @@ import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall, suppor import { Filter, IFilterDefinition } from "./filter"; import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler'; import * as utils from './utils'; -import { fixNotificationCountOnDecryption, sleep } from './utils'; +import { sleep } from './utils'; import { Direction, EventTimeline } from "./models/event-timeline"; import { IActionsObject, PushProcessor } from "./pushprocessor"; import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery"; @@ -9196,6 +9196,73 @@ export class MatrixClient extends TypedEventEmitter 0) { + // TODO: Handle mentions received while the client is offline + // See also https://github.com/vector-im/element-web/issues/9069 + const hasReadEvent = isThreadEvent + ? room.getThread(event.threadRootId).hasUserReadEvent(cli.getUserId(), event.getId()) + : room.hasUserReadEvent(cli.getUserId(), event.getId()); + + if (!hasReadEvent) { + let newCount = currentCount; + if (newHighlight && !oldHighlight) newCount++; + if (!newHighlight && oldHighlight) newCount--; + + if (isThreadEvent) { + room.setThreadUnreadNotificationCount( + event.threadRootId, + NotificationCountType.Highlight, + newCount, + ); + } else { + room.setUnreadNotificationCount(NotificationCountType.Highlight, newCount); + } + + // Fix 'Mentions Only' rooms from not having the right badge count + const totalCount = isThreadEvent + ? room.getThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Total) + : room.getUnreadNotificationCount(NotificationCountType.Total); + + if (totalCount < newCount) { + if (isThreadEvent) { + room.setThreadUnreadNotificationCount( + event.threadRootId, + NotificationCountType.Total, + newCount, + ); + } else { + room.setUnreadNotificationCount(NotificationCountType.Total, newCount); + } + } + } + } +} + /** * Fires whenever the SDK receives a new event. *

diff --git a/src/utils.ts b/src/utils.ts index 6254626eabd..818da7f647a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -27,8 +27,6 @@ import type * as NodeCrypto from "crypto"; import { MatrixEvent } from "./models/event"; import { M_TIMESTAMP } from "./@types/location"; import { ReceiptType } from "./@types/read_receipts"; -import { MatrixClient } from "./client"; -import { NotificationCountType } from "./models/room"; const interns = new Map(); @@ -675,67 +673,3 @@ export function sortEventsByLatestContentTimestamp(left: MatrixEvent, right: Mat export function isSupportedReceiptType(receiptType: string): boolean { return [ReceiptType.Read, ReceiptType.ReadPrivate].includes(receiptType as ReceiptType); } - -/** - * recalculates an accurate notifications count on event decryption. - * Servers do not have enough knowledge about encrypted events to calculate an - * accurate notification_count - */ -export function fixNotificationCountOnDecryption(cli: MatrixClient, event: MatrixEvent): void { - const oldActions = event.getPushActions(); - const actions = cli.getPushActionsForEvent(event, true); - - const room = cli.getRoom(event.getRoomId()); - if (!room) return; - - const isThreadEvent = !!event.threadRootId; - const currentCount = isThreadEvent - ? room.getThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Highlight) - : room.getUnreadNotificationCount(NotificationCountType.Highlight); - - // Ensure the unread counts are kept up to date if the event is encrypted - // We also want to make sure that the notification count goes up if we already - // have encrypted events to avoid other code from resetting 'highlight' to zero. - const oldHighlight = !!oldActions?.tweaks?.highlight; - const newHighlight = !!actions?.tweaks?.highlight; - if (oldHighlight !== newHighlight || currentCount > 0) { - // TODO: Handle mentions received while the client is offline - // See also https://github.com/vector-im/element-web/issues/9069 - const hasReadEvent = isThreadEvent - ? room.getThread(event.threadRootId).hasUserReadEvent(cli.getUserId(), event.getId()) - : room.hasUserReadEvent(cli.getUserId(), event.getId()); - - if (!hasReadEvent) { - let newCount = currentCount; - if (newHighlight && !oldHighlight) newCount++; - if (!newHighlight && oldHighlight) newCount--; - - if (isThreadEvent) { - room.setThreadUnreadNotificationCount( - event.threadRootId, - NotificationCountType.Highlight, - newCount, - ); - } else { - room.setUnreadNotificationCount(NotificationCountType.Highlight, newCount); - } - - // Fix 'Mentions Only' rooms from not having the right badge count - const totalCount = isThreadEvent - ? room.getThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Total) - : room.getUnreadNotificationCount(NotificationCountType.Total); - - if (totalCount < newCount) { - if (isThreadEvent) { - room.setThreadUnreadNotificationCount( - event.threadRootId, - NotificationCountType.Total, - newCount, - ); - } else { - room.setUnreadNotificationCount(NotificationCountType.Total, newCount); - } - } - } - } -} From 7d5adf48bcca8fcdcefea707528aa906c71929e2 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 3 Oct 2022 17:40:56 +0100 Subject: [PATCH 05/13] Use map over plain JS object --- src/models/room.ts | 16 ++++++++++------ src/sync.ts | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/models/room.ts b/src/models/room.ts index c8c9d2a990a..9cfb1a09f32 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -186,7 +186,7 @@ export class Room extends ReadReceipt { public readonly reEmitter: TypedReEmitter; private txnToEvent: Record = {}; // Pending in-flight requests { string: MatrixEvent } private notificationCounts: NotificationCount = {}; - public threadNotifications: Record = {}; + private threadNotifications: Map = new Map(); private readonly timelineSets: EventTimelineSet[]; public readonly threadsTimelineSets: EventTimelineSet[] = []; // any filtered timeline sets we're maintaining for this room @@ -1191,7 +1191,7 @@ export class Room extends ReadReceipt { * for this type. */ public getThreadUnreadNotificationCount(threadId: string, type = NotificationCountType.Total): number | undefined { - return this.threadNotifications[threadId]?.[type]; + return this.threadNotifications.get(threadId)?.[type]; } /** @@ -1201,13 +1201,17 @@ export class Room extends ReadReceipt { * @returns {void} */ public setThreadUnreadNotificationCount(threadId: string, type: NotificationCountType, count: number): void { - this.threadNotifications[threadId] = { - highlight: this.threadNotifications[threadId]?.highlight ?? 0, - total: this.threadNotifications[threadId]?.total ?? 0, + this.threadNotifications.set(threadId, { + highlight: this.threadNotifications.get(threadId)?.highlight ?? 0, + total: this.threadNotifications.get(threadId)?.total ?? 0, ...{ [type]: count, }, - }; + }); + } + + public resetThreadUnreadNotificationCount(): void { + this.threadNotifications.clear(); } /** diff --git a/src/sync.ts b/src/sync.ts index d1866bf2a92..4d5ac5e978c 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -1268,7 +1268,7 @@ export class SyncApi { } } - room.threadNotifications = {}; + room.resetThreadUnreadNotificationCount(); const unreadThreadNotifications = joinObj[UNREAD_THREAD_NOTIFICATIONS.name]; if (unreadThreadNotifications) { Object.entries(unreadThreadNotifications).forEach(([threadId, unreadNotification]) => { From 0ebbd2543651d538b4108ec65daf94df44b6a3c9 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 3 Oct 2022 17:49:43 +0100 Subject: [PATCH 06/13] Add room thread unread notification count tests --- spec/unit/room.spec.ts | 38 +++++++++++++++++++++++++++++++++++++- src/models/room.ts | 4 ++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index e79fb7110cc..d6891f369e3 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -32,7 +32,7 @@ import { RoomEvent, } from "../../src"; import { EventTimeline } from "../../src/models/event-timeline"; -import { Room } from "../../src/models/room"; +import { NotificationCountType, Room } from "../../src/models/room"; import { RoomState } from "../../src/models/room-state"; import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event"; import { TestClient } from "../TestClient"; @@ -2562,4 +2562,40 @@ describe("Room", function() { expect(client.roomNameGenerator).toHaveBeenCalled(); }); }); + + describe("thread notifications", () => { + let room; + + beforeEach(() => { + const client = new TestClient(userA).client; + room = new Room(roomId, client, userA); + }); + + it("defaults to undefined", () => { + expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBeUndefined(); + expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBeUndefined(); + }); + + it("lets you set values", () => { + room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 1); + + expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(1); + expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBeUndefined(); + + room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 10); + + expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(1); + expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBe(10); + }); + + it("lets you reset threads notifications", () => { + room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 666); + room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 123); + + room.resetThreadUnreadNotificationCount(); + + expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBeUndefined(); + expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBeUndefined(); + }); + }); }); diff --git a/src/models/room.ts b/src/models/room.ts index 9cfb1a09f32..c8a31f00da5 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -1202,8 +1202,8 @@ export class Room extends ReadReceipt { */ public setThreadUnreadNotificationCount(threadId: string, type: NotificationCountType, count: number): void { this.threadNotifications.set(threadId, { - highlight: this.threadNotifications.get(threadId)?.highlight ?? 0, - total: this.threadNotifications.get(threadId)?.total ?? 0, + highlight: this.threadNotifications.get(threadId)?.highlight, + total: this.threadNotifications.get(threadId)?.total, ...{ [type]: count, }, From e05a8c9d818b36212135db030974e485c7d9fca1 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 3 Oct 2022 17:52:29 +0100 Subject: [PATCH 07/13] Add filter tests --- spec/unit/filter.spec.ts | 13 +++++++++++++ src/filter.ts | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/spec/unit/filter.spec.ts b/spec/unit/filter.spec.ts index faa0f53cad6..925915729c9 100644 --- a/spec/unit/filter.spec.ts +++ b/spec/unit/filter.spec.ts @@ -43,4 +43,17 @@ describe("Filter", function() { expect(filter.getDefinition()).toEqual(definition); }); }); + + describe("setUnreadThreadNotifications", function() { + it("setUnreadThreadNotifications", function() { + filter.setUnreadThreadNotifications(true); + expect(filter.getDefinition()).toEqual({ + room: { + timeline: { + unread_thread_notifications: true, + }, + }, + }); + }); + }); }); diff --git a/src/filter.ts b/src/filter.ts index 496e18ece99..e4847d54555 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -228,7 +228,7 @@ export class Filter { * @param {boolean} enabled */ public setUnreadThreadNotifications(enabled: boolean): void { - setProp(this.definition, `room.timeline.${UNREAD_THREAD_NOTIFICATIONS.name}`, !!enabled); + setProp(this.definition, "room.timeline.unread_thread_notifications", !!enabled); } setLazyLoadMembers(enabled: boolean): void { From 2457003df0ff4613829cd503fc8bc9e1e93d22dd Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 3 Oct 2022 17:59:15 +0100 Subject: [PATCH 08/13] Add sync-accumulator tests --- spec/unit/sync-accumulator.spec.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/spec/unit/sync-accumulator.spec.ts b/spec/unit/sync-accumulator.spec.ts index 645efbfbba4..5618dbe2239 100644 --- a/spec/unit/sync-accumulator.spec.ts +++ b/spec/unit/sync-accumulator.spec.ts @@ -30,6 +30,12 @@ const RES_WITH_AGE = { account_data: { events: [] }, ephemeral: { events: [] }, unread_notifications: {}, + unread_thread_notifications: { + "$143273582443PhrSn:example.org": { + highlight_count: 0, + notification_count: 1, + }, + }, timeline: { events: [ Object.freeze({ @@ -439,6 +445,13 @@ describe("SyncAccumulator", function() { Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]), ); }); + + it("should retrieve unread thread notifications", () => { + sa.accumulate(RES_WITH_AGE); + const output = sa.getJSON(); + expect(output.roomsData.join["!foo:bar"] + .unread_thread_notifications["$143273582443PhrSn:example.org"]).not.toBeUndefined(); + }); }); }); From 211d8a9f618d52325d948e33fe014a6b37c01f9c Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 3 Oct 2022 21:05:46 +0100 Subject: [PATCH 09/13] Lint fix --- src/filter.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/filter.ts b/src/filter.ts index e4847d54555..0cf2d1c99e5 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -22,7 +22,6 @@ import { EventType, RelationType, } from "./@types/event"; -import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync"; import { FilterComponent, IFilterComponent } from "./filter-component"; import { MatrixEvent } from "./models/event"; From 7ab13a39d26eb729c1bef73805789a7ef05f7b87 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 4 Oct 2022 10:45:17 +0100 Subject: [PATCH 10/13] Add fixNotificationCountOnDecryptionTest --- package.json | 1 + spec/test-utils/client.ts | 94 ++++++++++++++++++++++++++ spec/unit/notifications.spec.ts | 114 ++++++++++++++++++++++++++++++++ src/client.ts | 18 ++--- src/models/read-receipt.ts | 4 +- yarn.lock | 27 +++++++- 6 files changed, 247 insertions(+), 11 deletions(-) create mode 100644 spec/test-utils/client.ts create mode 100644 spec/unit/notifications.spec.ts diff --git a/package.json b/package.json index 8395a22659a..6be8b75c5b6 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "fake-indexeddb": "^4.0.0", "jest": "^29.0.0", "jest-localstorage-mock": "^2.4.6", + "jest-mock": "^27.5.1", "jest-sonar-reporter": "^2.0.0", "jsdoc": "^3.6.6", "matrix-mock-request": "^2.1.2", diff --git a/spec/test-utils/client.ts b/spec/test-utils/client.ts new file mode 100644 index 00000000000..3cacd179d14 --- /dev/null +++ b/spec/test-utils/client.ts @@ -0,0 +1,94 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MethodKeysOf, mocked, MockedObject } from "jest-mock"; + +import { ClientEventHandlerMap, EmittedEvents, MatrixClient } from "../../src/client"; +import { TypedEventEmitter } from "../../src/models/typed-event-emitter"; +import { User } from "../../src/models/user"; + +/** + * Mock client with real event emitter + * useful for testing code that listens + * to MatrixClient events + */ +export class MockClientWithEventEmitter extends TypedEventEmitter { + constructor(mockProperties: Partial, unknown>> = {}) { + super(); + Object.assign(this, mockProperties); + } +} + +/** + * - make a mock client + * - cast the type to mocked(MatrixClient) + * - spy on MatrixClientPeg.get to return the mock + * eg + * ``` + * const mockClient = getMockClientWithEventEmitter({ + getUserId: jest.fn().mockReturnValue(aliceId), + }); + * ``` + */ +export const getMockClientWithEventEmitter = ( + mockProperties: Partial, unknown>>, +): MockedObject => { + const mock = mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient); + return mock; +}; + +/** + * Returns basic mocked client methods related to the current user + * ``` + * const mockClient = getMockClientWithEventEmitter({ + ...mockClientMethodsUser('@mytestuser:domain'), + }); + * ``` + */ +export const mockClientMethodsUser = (userId = '@alice:domain') => ({ + getUserId: jest.fn().mockReturnValue(userId), + getUser: jest.fn().mockReturnValue(new User(userId)), + isGuest: jest.fn().mockReturnValue(false), + mxcUrlToHttp: jest.fn().mockReturnValue('mock-mxcUrlToHttp'), + credentials: { userId }, + getThreePids: jest.fn().mockResolvedValue({ threepids: [] }), + getAccessToken: jest.fn(), +}); + +/** + * Returns basic mocked client methods related to rendering events + * ``` + * const mockClient = getMockClientWithEventEmitter({ + ...mockClientMethodsUser('@mytestuser:domain'), + }); + * ``` + */ +export const mockClientMethodsEvents = () => ({ + decryptEventIfNeeded: jest.fn(), + getPushActionsForEvent: jest.fn(), +}); + +/** + * Returns basic mocked client methods related to server support + */ +export const mockClientMethodsServer = (): Partial, unknown>> => ({ + doesServerSupportSeparateAddAndBind: jest.fn(), + getIdentityServerUrl: jest.fn(), + getHomeserverUrl: jest.fn(), + getCapabilities: jest.fn().mockReturnValue({}), + doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false), +}); + diff --git a/spec/unit/notifications.spec.ts b/spec/unit/notifications.spec.ts new file mode 100644 index 00000000000..89601327bc4 --- /dev/null +++ b/spec/unit/notifications.spec.ts @@ -0,0 +1,114 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { + EventType, + fixNotificationCountOnDecryption, + MatrixClient, + MatrixEvent, + MsgType, + NotificationCountType, + RelationType, + Room, +} from "../../src/matrix"; +import { IActionsObject } from "../../src/pushprocessor"; +import { ReEmitter } from "../../src/ReEmitter"; +import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../test-utils/client"; +import { mkEvent, mock } from "../test-utils/test-utils"; + +let mockClient: MatrixClient; +let room: Room; +let event: MatrixEvent; +let threadEvent: MatrixEvent; + +const ROOM_ID = "!roomId:example.org"; +let THREAD_ID; + +function mkPushAction(notify, highlight): IActionsObject { + return { + notify, + tweaks: { + highlight, + }, + }; +} + +describe("fixNotificationCountOnDecryption", () => { + beforeEach(() => { + mockClient = getMockClientWithEventEmitter({ + ...mockClientMethodsUser(), + getPushActionsForEvent: jest.fn().mockReturnValue(mkPushAction(true, true)), + getRoom: jest.fn().mockImplementation(() => room), + decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0), + supportsExperimentalThreads: jest.fn().mockReturnValue(true), + }); + mockClient.reEmitter = mock(ReEmitter, 'ReEmitter'); + + room = new Room(ROOM_ID, mockClient, mockClient.getUserId()); + room.setUnreadNotificationCount(NotificationCountType.Total, 1); + room.setUnreadNotificationCount(NotificationCountType.Highlight, 0); + + event = mkEvent({ + type: EventType.RoomMessage, + content: { + msgtype: MsgType.Text, + body: "Hello world!", + }, + event: true, + }, mockClient); + + THREAD_ID = event.getId(); + threadEvent = mkEvent({ + type: EventType.RoomMessage, + content: { + "m.relates_to": { + rel_type: RelationType.Thread, + event_id: THREAD_ID, + }, + "msgtype": MsgType.Text, + "body": "Thread reply", + }, + event: true, + }); + room.createThread(THREAD_ID, event, [threadEvent], false); + + room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 1); + room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0); + + event.getPushActions = jest.fn().mockReturnValue(mkPushAction(false, false)); + threadEvent.getPushActions = jest.fn().mockReturnValue(mkPushAction(false, false)); + }); + + it("changes the room count to highlight on decryption", () => { + expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(1); + expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0); + + fixNotificationCountOnDecryption(mockClient, event); + + expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(1); + expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1); + }); + + it("changes the thread count to highlight on decryption", () => { + expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1); + expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0); + + fixNotificationCountOnDecryption(mockClient, threadEvent); + + expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1); + expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(1); + }); +}); diff --git a/src/client.ts b/src/client.ts index 6f724494f35..22101a9db45 100644 --- a/src/client.ts +++ b/src/client.ts @@ -865,7 +865,7 @@ type UserEvents = UserEvent.AvatarUrl | UserEvent.CurrentlyActive | UserEvent.LastPresenceTs; -type EmittedEvents = ClientEvent +export type EmittedEvents = ClientEvent | RoomEvents | RoomStateEvents | CryptoEvents @@ -1080,7 +1080,9 @@ export class MatrixClient extends TypedEventEmitter fixNotificationCountOnDecryption(this, event)); + this.on(MatrixEventEvent.Decrypted, (event) => { + fixNotificationCountOnDecryption(this, event); + }); // Like above, we have to listen for read receipts from ourselves in order to // correctly handle notification counts on encrypted rooms. @@ -9206,15 +9208,15 @@ export function fixNotificationCountOnDecryption(cli: MatrixClient, event: Matri const actions = cli.getPushActionsForEvent(event, true); const room = cli.getRoom(event.getRoomId()); - if (!room) return; + if (!room || !cli.getUserId()) return; - const isThreadEvent = !!event.threadRootId; - const currentCount = isThreadEvent + const isThreadEvent = !!event.threadRootId && !event.isThreadRoot; + const currentCount = (isThreadEvent ? room.getThreadUnreadNotificationCount( event.threadRootId, NotificationCountType.Highlight, ) - : room.getUnreadNotificationCount(NotificationCountType.Highlight); + : room.getUnreadNotificationCount(NotificationCountType.Highlight)) ?? 0; // Ensure the unread counts are kept up to date if the event is encrypted // We also want to make sure that the notification count goes up if we already @@ -9244,9 +9246,9 @@ export function fixNotificationCountOnDecryption(cli: MatrixClient, event: Matri } // Fix 'Mentions Only' rooms from not having the right badge count - const totalCount = isThreadEvent + const totalCount = (isThreadEvent ? room.getThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Total) - : room.getUnreadNotificationCount(NotificationCountType.Total); + : room.getUnreadNotificationCount(NotificationCountType.Total)) ?? 0; if (totalCount < newCount) { if (isThreadEvent) { diff --git a/src/models/read-receipt.ts b/src/models/read-receipt.ts index 1f7f5726f9e..e6d558766dc 100644 --- a/src/models/read-receipt.ts +++ b/src/models/read-receipt.ts @@ -282,7 +282,7 @@ export abstract class ReadReceipt< const readUpToId = this.getEventReadUpTo(userId, false); if (readUpToId === eventId) return true; - if (this.timeline.length + if (this.timeline?.length && this.timeline[this.timeline.length - 1].getSender() && this.timeline[this.timeline.length - 1].getSender() === userId) { // It doesn't matter where the event is in the timeline, the user has read @@ -290,7 +290,7 @@ export abstract class ReadReceipt< return true; } - for (let i = this.timeline.length - 1; i >= 0; --i) { + for (let i = this.timeline?.length - 1; i >= 0; --i) { const ev = this.timeline[i]; // If we encounter the target event first, the user hasn't read it diff --git a/yarn.lock b/yarn.lock index 8965240fda9..72832e28d6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1284,6 +1284,17 @@ slash "^3.0.0" write-file-atomic "^4.0.1" +"@jest/types@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" + integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^16.0.0" + chalk "^4.0.0" + "@jest/types@^28.1.3": version "28.1.3" resolved "https://registry.yarnpkg.com/@jest/types/-/types-28.1.3.tgz#b05de80996ff12512bc5ceb1d208285a7d11748b" @@ -1358,7 +1369,6 @@ "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz": version "3.2.12" - uid "0bce3c86f9d36a4984d3c3e07df1c3fb4c679bd9" resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz#0bce3c86f9d36a4984d3c3e07df1c3fb4c679bd9" "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": @@ -1695,6 +1705,13 @@ resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== +"@types/yargs@^16.0.0": + version "16.0.4" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977" + integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw== + dependencies: + "@types/yargs-parser" "*" + "@types/yargs@^17.0.8": version "17.0.13" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.13.tgz#34cced675ca1b1d51fcf4d34c3c6f0fa142a5c76" @@ -4471,6 +4488,14 @@ jest-message-util@^29.1.2: slash "^3.0.0" stack-utils "^2.0.3" +jest-mock@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.5.1.tgz#19948336d49ef4d9c52021d34ac7b5f36ff967d6" + integrity sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og== + dependencies: + "@jest/types" "^27.5.1" + "@types/node" "*" + jest-mock@^29.1.2: version "29.1.2" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.1.2.tgz#de47807edbb9d4abf8423f1d8d308d670105678c" From 39c3a491f19e811800e848328b715a0a4f690c0e Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 4 Oct 2022 10:58:47 +0100 Subject: [PATCH 11/13] Add sync integration test --- spec/integ/matrix-client-syncing.spec.ts | 69 ++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/spec/integ/matrix-client-syncing.spec.ts b/spec/integ/matrix-client-syncing.spec.ts index e07e52c7b83..f1c43c4f904 100644 --- a/spec/integ/matrix-client-syncing.spec.ts +++ b/spec/integ/matrix-client-syncing.spec.ts @@ -29,7 +29,9 @@ import { MatrixClient, ClientEvent, IndexedDBCryptoStore, + NotificationCountType, } from "../../src"; +import { UNREAD_THREAD_NOTIFICATIONS } from '../../src/@types/sync'; import * as utils from "../test-utils/test-utils"; import { TestClient } from "../TestClient"; @@ -1363,6 +1365,73 @@ describe("MatrixClient syncing", () => { }); }); + describe("unread notifications", () => { + const THREAD_ID = "$ThisIsARandomEventId"; + + const syncData = { + rooms: { + join: { + [roomOne]: { + timeline: { + events: [ + utils.mkMessage({ + room: roomOne, user: otherUserId, msg: "hello", + }), + utils.mkMessage({ + room: roomOne, user: otherUserId, msg: "world", + }), + ], + }, + state: { + events: [ + utils.mkEvent({ + type: "m.room.name", room: roomOne, user: otherUserId, + content: { + name: "Room name", + }, + }), + utils.mkMembership({ + room: roomOne, mship: "join", user: otherUserId, + }), + utils.mkMembership({ + room: roomOne, mship: "join", user: selfUserId, + }), + utils.mkEvent({ + type: "m.room.create", room: roomOne, user: selfUserId, + content: { + creator: selfUserId, + }, + }), + ], + }, + }, + }, + }, + }; + it("should sync unread notifications.", () => { + syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = { + [THREAD_ID]: { + "highlight_count": 2, + "notification_count": 5, + }, + }; + + httpBackend!.when("GET", "/sync").respond(200, syncData); + + client!.startClient(); + + return Promise.all([ + httpBackend!.flushAllExpected(), + awaitSyncEvent(), + ]).then(() => { + const room = client!.getRoom(roomOne); + + expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5); + expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(2); + }); + }); + }); + describe("of a room", () => { xit("should sync when a join event (which changes state) for the user" + " arrives down the event stream (e.g. join from another device)", () => { From 2f8c6d05d8935380f8378562fe6707db95680335 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 5 Oct 2022 09:17:30 +0100 Subject: [PATCH 12/13] Make sync path more flexible --- src/sync.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sync.ts b/src/sync.ts index 4d5ac5e978c..8342617ba32 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -1269,7 +1269,8 @@ export class SyncApi { } room.resetThreadUnreadNotificationCount(); - const unreadThreadNotifications = joinObj[UNREAD_THREAD_NOTIFICATIONS.name]; + const unreadThreadNotifications = joinObj[UNREAD_THREAD_NOTIFICATIONS.name] + ?? joinObj[UNREAD_THREAD_NOTIFICATIONS.altName]; if (unreadThreadNotifications) { Object.entries(unreadThreadNotifications).forEach(([threadId, unreadNotification]) => { room.setThreadUnreadNotificationCount( @@ -1278,9 +1279,9 @@ export class SyncApi { unreadNotification.notification_count, ); - const hasUnreadNotification = + const hasNoNotifications = room.getThreadUnreadNotificationCount(threadId, NotificationCountType.Highlight) <= 0; - if (!encrypted || (encrypted && hasUnreadNotification)) { + if (!encrypted || (encrypted && hasNoNotifications)) { room.setThreadUnreadNotificationCount( threadId, NotificationCountType.Highlight, From 7da5957ff9b8385a534ef84b5257c6f334648418 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 5 Oct 2022 09:21:25 +0100 Subject: [PATCH 13/13] Add support check with matrix version --- src/sync.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sync.ts b/src/sync.ts index 8342617ba32..0026831d591 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -707,7 +707,8 @@ export class SyncApi { initialFilter.setDefinition(filter.getDefinition()); initialFilter.setTimelineLimit(this.opts.initialSyncLimit); const supportsThreadNotifications = - await this.client.doesServerSupportUnstableFeature("org.matrix.msc3773"); + await this.client.doesServerSupportUnstableFeature("org.matrix.msc3773") + || await this.client.isVersionSupported("v1.4"); initialFilter.setUnreadThreadNotifications(supportsThreadNotifications); // Use an inline filter, no point uploading it for a single usage firstSyncFilter = JSON.stringify(initialFilter.getDefinition());