Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Threads notification proof of concept (MSC3773) #2600

Closed
Closed
Show file tree
Hide file tree
Changes from 5 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
9 changes: 9 additions & 0 deletions src/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export interface IRoomEventFilter extends IFilterComponent {
types?: Array<EventType | string>;
related_by_senders?: Array<RelationType | string>;
related_by_rel_types?: string[];
unread_thread_notifications?: boolean;

// Unstable values
"io.element.relation_senders"?: Array<RelationType | string>;
Expand Down Expand Up @@ -220,6 +221,14 @@ export class Filter {
setProp(this.definition, "room.timeline.limit", limit);
}

/**
* Enable threads unread notification
* @param {boolean} enabled
*/
public setUnreadThreadNotifications(enabled: boolean) {
setProp(this.definition, "room.timeline.unread_thread_notifications", enabled);
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
}

setLazyLoadMembers(enabled: boolean) {
setProp(this.definition, "room.state.lazy_load_members", !!enabled);
}
Expand Down
65 changes: 35 additions & 30 deletions src/models/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,13 @@ export type RoomEventHandlerMap = {
[ThreadEvent.New]: (thread: Thread, toStartOfTimeline: boolean) => void;
} & ThreadHandlerMap & MatrixEventHandlerMap;

type NotificationCount = Partial<Record<NotificationCountType, number>>;

export class Room extends TimelineReceipts<EmittedEvents, RoomEventHandlerMap> {
public readonly reEmitter: TypedReEmitter<EmittedEvents, RoomEventHandlerMap>;
private txnToEvent: Record<string, MatrixEvent> = {}; // Pending in-flight requests { string: MatrixEvent }
private notificationCounts: Partial<Record<NotificationCountType, number>> = {};
private notificationCounts: NotificationCount = {};
public threadNotifications: Record<string, NotificationCount> = {};
private readonly timelineSets: EventTimelineSet[];
public readonly threadsTimelineSets: EventTimelineSet[] = [];
// any filtered timeline sets we're maintaining for this room
Expand Down Expand Up @@ -1120,14 +1123,30 @@ export class Room extends TimelineReceipts<EmittedEvents, RoomEventHandlerMap> {
return event;
}

/**
* Get sum of threads and roon notification counts
* @param {String} type The type of notification count to get. default: 'total'
* @return {Number} The notification count.
*/
public getTotalUnreadNotificationCount(type = NotificationCountType.Total): number {
return (this.getUnreadNotificationCount(type) ?? 0) +
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
this.getTotalThreadsUnreadNotificationCount(type);
}

public getTotalThreadsUnreadNotificationCount(type = NotificationCountType.Total): number {
return Object.keys(this.threadNotifications).reduce((total: number, threadId: string) => {
return total + (this.getThreadUnreadNotificationCount(threadId, type) ?? 0);
}, 0);
}

/**
* Get one of the notification counts for this room
* @param {String} type The type of notification count to get. default: 'total'
* @return {Number} The notification count, or undefined if there is no count
* for this type.
*/
public getUnreadNotificationCount(type = NotificationCountType.Total): number | undefined {
return this.notificationCounts[type];
return this.notificationCounts[type] ?? 0;
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand All @@ -1139,6 +1158,20 @@ export class Room extends TimelineReceipts<EmittedEvents, RoomEventHandlerMap> {
this.notificationCounts[type] = count;
}

public getThreadUnreadNotificationCount(threadId: string, type = NotificationCountType.Total): number | undefined {
return this.threadNotifications[threadId]?.[type];
}

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,
},
};
}

public setSummary(summary: IRoomSummary): void {
const heroes = summary["m.heroes"];
const joinedCount = summary["m.joined_member_count"];
Expand Down Expand Up @@ -2463,24 +2496,6 @@ export class Room extends TimelineReceipts<EmittedEvents, RoomEventHandlerMap> {
});
}

/**
* Gets the latest receipt for a given user in the room
* @param userId The id of the user for which we want the receipt
* @param ignoreSynthesized Whether to ignore synthesized receipts or not
* @param receiptType Optional. The type of the receipt we want to get
* @returns the latest receipts of the chosen type for the chosen user
*/
public getReadReceiptForUserId(
userId: string, ignoreSynthesized = false, receiptType = ReceiptType.Read,
): IWrappedReceipt | null {
const [realReceipt, syntheticReceipt] = this.receipts[receiptType]?.[userId] ?? [];
if (ignoreSynthesized) {
return realReceipt;
}

return syntheticReceipt ?? realReceipt;
}

/**
* Get the ID of the event that a given user has read up to, or null if we
* have received no read receipts from them.
Expand Down Expand Up @@ -2585,16 +2600,6 @@ export class Room extends TimelineReceipts<EmittedEvents, RoomEventHandlerMap> {
return false;
}

/**
* Get a list of receipts for the given event.
* @param {MatrixEvent} event the event to get receipts for
* @return {Object[]} A list of receipts with a userId, type and data keys or
* an empty list.
*/
public getReceiptsForEvent(event: MatrixEvent): ICachedReceipt[] {
return this.receiptCacheByEventId[event.getId()] || [];
}

/**
* Add a receipt event to the room.
* @param {MatrixEvent} event The m.receipt event.
Expand Down
23 changes: 17 additions & 6 deletions src/sync-accumulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ export interface IJoinedRoom {
ephemeral: IEphemeral;
account_data: IAccountData;
unread_notifications: IUnreadNotificationCounts;
unread_thread_notifications?: {
[threadId: string]: IUnreadNotificationCounts;
};
}

export interface IStrippedState {
Expand Down Expand Up @@ -154,6 +157,9 @@ interface IRoom {
_summary: Partial<IRoomSummary>;
_accountData: { [eventType: string]: IMinimalEvent };
_unreadNotifications: Partial<IUnreadNotificationCounts>;
_unreadThreadNotifications?: {
[threadId: string]: Partial<IUnreadNotificationCounts>;
};
_readReceipts: {
[userId: string]: {
data: IMinimalEvent;
Expand Down Expand Up @@ -358,12 +364,13 @@ export class SyncAccumulator {
// Create truly empty objects so event types of 'hasOwnProperty' and co
// don't cause this code to break.
this.joinRooms[roomId] = {
_currentState: Object.create(null),
_timeline: [],
_accountData: Object.create(null),
_unreadNotifications: {},
_summary: {},
_readReceipts: {},
"_currentState": Object.create(null),
"_timeline": [],
"_accountData": Object.create(null),
"_unreadNotifications": {},
"_unreadThreadNotifications": {},
"_summary": {},
"_readReceipts": {},
};
}
const currentData = this.joinRooms[roomId];
Expand All @@ -379,6 +386,9 @@ export class SyncAccumulator {
if (data.unread_notifications) {
currentData._unreadNotifications = data.unread_notifications;
}
if (data.unread_thread_notifications) {
currentData._unreadThreadNotifications = data.unread_thread_notifications;
}
if (data.summary) {
const HEROES_KEY = "m.heroes";
const INVITED_COUNT_KEY = "m.invited_member_count";
Expand Down Expand Up @@ -537,6 +547,7 @@ export class SyncAccumulator {
prev_batch: null,
},
unread_notifications: roomData._unreadNotifications,
unread_thread_notifications: roomData._unreadThreadNotifications,
summary: roomData._summary as IRoomSummary,
};
// Add account data
Expand Down
27 changes: 25 additions & 2 deletions src/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,7 @@ export class SyncApi {
const buildDefaultFilter = () => {
const filter = new Filter(client.credentials.userId);
filter.setTimelineLimit(this.opts.initialSyncLimit);
filter.setUnreadThreadNotifications(true);
return filter;
};

Expand Down Expand Up @@ -996,8 +997,8 @@ export class SyncApi {
}

const qps: ISyncParams = {
filter: filterId,
timeout: pollTimeout,
"filter": filterId,
"timeout": pollTimeout,
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
};

if (this.opts.disablePresence) {
Expand Down Expand Up @@ -1299,6 +1300,28 @@ export class SyncApi {
}
}

room.threadNotifications = {};
const unreadThreadNotifications = joinObj.unread_thread_notifications;
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) {
Expand Down