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

Commit

Permalink
Add labs flag for Threads Activity Centre (#12137)
Browse files Browse the repository at this point in the history
* Add `Thread Activity centre` labs flag

* Rename translation string

* Update supportedLevels

* Fix labs subsection test

* Update Threads Activity Centre label

* Make threads activity centre labs flag split out unread counts

Just shows notif & unread counts for main thread if the TAC is enabled.

* Fix tests

* Simpler fix

* Pass in & cache the status of the TAC labs flag

* Pass includeThreads as setting to doesRoomHaveUnreadMessages too

* Fix tests

---------

Co-authored-by: David Baker <[email protected]>
  • Loading branch information
florianduros and dbkr authored Jan 29, 2024
1 parent a370a5c commit 77e1649
Show file tree
Hide file tree
Showing 15 changed files with 111 additions and 56 deletions.
35 changes: 29 additions & 6 deletions src/RoomNotifs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,19 @@ export function setRoomNotifsState(client: MatrixClient, roomId: string, newStat
}
}

export function getUnreadNotificationCount(room: Room, type: NotificationCountType, threadId?: string): number {
export function getUnreadNotificationCount(
room: Room,
type: NotificationCountType,
includeThreads: boolean,
threadId?: string,
): number {
const getCountShownForRoom = (r: Room, type: NotificationCountType): number => {
return includeThreads ? r.getUnreadNotificationCount(type) : r.getRoomUnreadNotificationCount(type);
};

let notificationCount = !!threadId
? room.getThreadUnreadNotificationCount(threadId, type)
: room.getUnreadNotificationCount(type);
: getCountShownForRoom(room, type);

// Check notification counts in the old room just in case there's some lost
// there. We only go one level down to avoid performance issues, and theory
Expand All @@ -99,7 +108,7 @@ export function getUnreadNotificationCount(room: Room, type: NotificationCountTy
// notifying the user for unread messages because they would have extreme
// difficulty changing their notification preferences away from "All Messages"
// and "Noisy".
notificationCount += oldRoom.getUnreadNotificationCount(NotificationCountType.Highlight);
notificationCount += getCountShownForRoom(oldRoom, NotificationCountType.Highlight);
}
}

Expand Down Expand Up @@ -224,9 +233,18 @@ function isMuteRule(rule: IPushRule): boolean {
);
}

/**
* Returns an object giving information about the unread state of a room or thread
* @param room The room to query, or the room the thread is in
* @param threadId The thread to check the unread state of, or undefined to query the main thread
* @param includeThreads If threadId is undefined, true to include threads other than the main thread, or
* false to exclude them. Ignored if threadId is specified.
* @returns
*/
export function determineUnreadState(
room?: Room,
threadId?: string,
includeThreads?: boolean,
): { level: NotificationLevel; symbol: string | null; count: number } {
if (!room) {
return { symbol: null, count: 0, level: NotificationLevel.None };
Expand All @@ -248,8 +266,13 @@ export function determineUnreadState(
return { symbol: null, count: 0, level: NotificationLevel.None };
}

const redNotifs = getUnreadNotificationCount(room, NotificationCountType.Highlight, threadId);
const greyNotifs = getUnreadNotificationCount(room, NotificationCountType.Total, threadId);
const redNotifs = getUnreadNotificationCount(
room,
NotificationCountType.Highlight,
includeThreads ?? false,
threadId,
);
const greyNotifs = getUnreadNotificationCount(room, NotificationCountType.Total, includeThreads ?? false, threadId);

const trueCount = greyNotifs || redNotifs;
if (redNotifs > 0) {
Expand All @@ -269,7 +292,7 @@ export function determineUnreadState(
}
// If the thread does not exist, assume it contains no unreads
} else {
hasUnread = doesRoomHaveUnreadMessages(room);
hasUnread = doesRoomHaveUnreadMessages(room, includeThreads ?? false);
}

return {
Expand Down
9 changes: 7 additions & 2 deletions src/Unread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,19 @@ export function eventTriggersUnreadCount(client: MatrixClient, ev: MatrixEvent):
return haveRendererForEvent(ev, client, false /* hidden messages should never trigger unread counts anyways */);
}

export function doesRoomHaveUnreadMessages(room: Room): boolean {
export function doesRoomHaveUnreadMessages(room: Room, includeThreads: boolean): boolean {
if (SettingsStore.getValue("feature_sliding_sync")) {
// TODO: https://github.com/vector-im/element-web/issues/23207
// Sliding Sync doesn't support unread indicator dots (yet...)
return false;
}

for (const withTimeline of [room, ...room.getThreads()]) {
const toCheck: Array<Room | Thread> = [room];
if (includeThreads) {
toCheck.push(...room.getThreads());
}

for (const withTimeline of toCheck) {
if (doesTimelineHaveUnreadMessages(room, withTimeline.timeline)) {
// We found an unread, so the room is unread
return true;
Expand Down
7 changes: 5 additions & 2 deletions src/components/views/dialogs/devtools/RoomNotifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ limitations under the License.
*/

import { NotificationCountType, Room, Thread, ReceiptType } from "matrix-js-sdk/src/matrix";
import React, { useContext } from "react";
import React, { useContext, useMemo } from "react";
import { ReadReceipt } from "matrix-js-sdk/src/models/read-receipt";

import MatrixClientContext from "../../../../contexts/MatrixClientContext";
Expand All @@ -25,6 +25,7 @@ import { determineUnreadState } from "../../../../RoomNotifs";
import { humanReadableNotificationLevel } from "../../../../stores/notifications/NotificationLevel";
import { doesRoomOrThreadHaveUnreadMessages } from "../../../../Unread";
import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool";
import SettingsStore from "../../../../settings/SettingsStore";

function UserReadUpTo({ target }: { target: ReadReceipt<any, any> }): JSX.Element {
const cli = useContext(MatrixClientContext);
Expand Down Expand Up @@ -65,10 +66,12 @@ function UserReadUpTo({ target }: { target: ReadReceipt<any, any> }): JSX.Elemen
}

export default function RoomNotifications({ onBack }: IDevtoolsProps): JSX.Element {
const tacEnabled = useMemo(() => SettingsStore.getValue("threadsActivityCentre"), []);

const { room } = useContext(DevtoolsContext);
const cli = useContext(MatrixClientContext);

const { level, count } = determineUnreadState(room);
const { level, count } = determineUnreadState(room, undefined, !tacEnabled);
const [notificationState] = useNotificationState(room);

return (
Expand Down
9 changes: 6 additions & 3 deletions src/hooks/useUnreadNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ limitations under the License.
*/

import { RoomEvent } from "matrix-js-sdk/src/matrix";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";

import type { NotificationCount, Room } from "matrix-js-sdk/src/matrix";
import { determineUnreadState } from "../RoomNotifs";
import { NotificationLevel } from "../stores/notifications/NotificationLevel";
import { useEventEmitter } from "./useEventEmitter";
import SettingsStore from "../settings/SettingsStore";

export const useUnreadNotifications = (
room?: Room,
Expand All @@ -30,6 +31,8 @@ export const useUnreadNotifications = (
count: number;
level: NotificationLevel;
} => {
const tacEnabled = useMemo(() => SettingsStore.getValue("threadsActivityCentre"), []);

const [symbol, setSymbol] = useState<string | null>(null);
const [count, setCount] = useState<number>(0);
const [level, setLevel] = useState<NotificationLevel>(NotificationLevel.None);
Expand All @@ -50,11 +53,11 @@ export const useUnreadNotifications = (
useEventEmitter(room, RoomEvent.MyMembership, () => updateNotificationState());

const updateNotificationState = useCallback(() => {
const { symbol, count, level } = determineUnreadState(room, threadId);
const { symbol, count, level } = determineUnreadState(room, threadId, !tacEnabled);
setSymbol(symbol);
setCount(count);
setLevel(level);
}, [room, threadId]);
}, [room, threadId, tacEnabled]);

useEffect(() => {
updateNotificationState();
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1416,6 +1416,7 @@
"group_rooms": "Rooms",
"group_spaces": "Spaces",
"group_themes": "Themes",
"group_threads": "Threads",
"group_voip": "Voice & Video",
"group_widgets": "Widgets",
"hidebold": "Hide notification dot (only display counters badges)",
Expand Down Expand Up @@ -1459,6 +1460,7 @@
"sliding_sync_server_no_support": "Your server lacks native support",
"sliding_sync_server_specify_proxy": "Your server lacks native support, you must specify a proxy",
"sliding_sync_server_support": "Your server has native support",
"threads_activity_centre": "Threads Activity Centre (in development). Currently this just removes thread notification counts from the count total in the room list",
"under_active_development": "Under active development.",
"unrealiable_e2e": "Unreliable in encrypted rooms",
"video_rooms": "Video rooms",
Expand Down
10 changes: 10 additions & 0 deletions src/settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export enum LabGroup {
Spaces,
Widgets,
Rooms,
Threads,
VoiceAndVideo,
Moderation,
Analytics,
Expand All @@ -104,6 +105,7 @@ export const labGroupNames: Record<LabGroup, TranslationKey> = {
[LabGroup.Spaces]: _td("labs|group_spaces"),
[LabGroup.Widgets]: _td("labs|group_widgets"),
[LabGroup.Rooms]: _td("labs|group_rooms"),
[LabGroup.Threads]: _td("labs|group_threads"),
[LabGroup.VoiceAndVideo]: _td("labs|group_voip"),
[LabGroup.Moderation]: _td("labs|group_moderation"),
[LabGroup.Analytics]: _td("common|analytics"),
Expand Down Expand Up @@ -1113,6 +1115,14 @@ export const SETTINGS: { [setting: string]: ISetting } = {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: [],
},
"threadsActivityCentre": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
labsGroup: LabGroup.Threads,
controller: new ReloadOnChangeController(),
displayName: _td("labs|threads_activity_centre"),
default: false,
isFeature: true,
},
[UIFeature.RoomHistorySettings]: {
supportedLevels: LEVELS_UI_FEATURE,
default: true,
Expand Down
7 changes: 5 additions & 2 deletions src/stores/notifications/RoomNotificationState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ import { NotificationState } from "./NotificationState";
import SettingsStore from "../../settings/SettingsStore";

export class RoomNotificationState extends NotificationState implements IDestroyable {
public constructor(public readonly room: Room) {
public constructor(
public readonly room: Room,
private includeThreads: boolean,
) {
super();
const cli = this.room.client;
this.room.on(RoomEvent.Receipt, this.handleReadReceipt);
Expand Down Expand Up @@ -90,7 +93,7 @@ export class RoomNotificationState extends NotificationState implements IDestroy
private updateNotificationState(): void {
const snapshot = this.snapshot();

const { level, symbol, count } = RoomNotifs.determineUnreadState(this.room);
const { level, symbol, count } = RoomNotifs.determineUnreadState(this.room, undefined, this.includeThreads);
const muted =
RoomNotifs.getRoomNotifsState(this.room.client, this.room.roomId) === RoomNotifs.RoomNotifState.Mute;
const knocked = SettingsStore.getValue("feature_ask_to_join") && this.room.getMyMembership() === "knock";
Expand Down
4 changes: 3 additions & 1 deletion src/stores/notifications/RoomNotificationStateStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
private listMap = new Map<TagID, ListNotificationState>();
private _globalState = new SummarizedNotificationState();

private tacEnabled = SettingsStore.getValue("threadsActivityCentre");

private constructor(dispatcher = defaultDispatcher) {
super(dispatcher, {});
SettingsStore.watchSetting("feature_dynamic_room_predecessors", null, () => {
Expand Down Expand Up @@ -97,7 +99,7 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
*/
public getRoomState(room: Room): RoomNotificationState {
if (!this.roomMap.has(room)) {
this.roomMap.set(room, new RoomNotificationState(room));
this.roomMap.set(room, new RoomNotificationState(room, !this.tacEnabled));
}
return this.roomMap.get(room)!;
}
Expand Down
36 changes: 18 additions & 18 deletions test/RoomNotifs-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,16 @@ describe("RoomNotifs test", () => {
});

it("counts room notification type", () => {
expect(getUnreadNotificationCount(room, NotificationCountType.Total)).toBe(0);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight)).toBe(0);
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(0);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, false)).toBe(0);
});

it("counts notifications type", () => {
room.setUnreadNotificationCount(NotificationCountType.Total, 2);
room.setUnreadNotificationCount(NotificationCountType.Highlight, 1);

expect(getUnreadNotificationCount(room, NotificationCountType.Total)).toBe(2);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight)).toBe(1);
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(2);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, false)).toBe(1);
});

describe("when there is a room predecessor", () => {
Expand Down Expand Up @@ -156,8 +156,8 @@ describe("RoomNotifs test", () => {
it("and there is a predecessor in the create event, it should count predecessor highlight", () => {
room.addLiveEvents([mkCreateEvent(OLD_ROOM_ID)]);

expect(getUnreadNotificationCount(room, NotificationCountType.Total)).toBe(8);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight)).toBe(7);
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(8);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, false)).toBe(7);
});
};

Expand All @@ -167,8 +167,8 @@ describe("RoomNotifs test", () => {
room.addLiveEvents([mkCreateEvent(OLD_ROOM_ID)]);
upsertRoomStateEvents(room, [mkPredecessorEvent(OLD_ROOM_ID)]);

expect(getUnreadNotificationCount(room, NotificationCountType.Total)).toBe(8);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight)).toBe(7);
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(8);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, false)).toBe(7);
});
};

Expand All @@ -195,8 +195,8 @@ describe("RoomNotifs test", () => {
room.addLiveEvents([mkCreateEvent()]);
upsertRoomStateEvents(room, [mkPredecessorEvent(OLD_ROOM_ID)]);

expect(getUnreadNotificationCount(room, NotificationCountType.Total)).toBe(2);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight)).toBe(1);
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(2);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, false)).toBe(1);
});
});

Expand All @@ -214,31 +214,31 @@ describe("RoomNotifs test", () => {
room.addLiveEvents([mkCreateEvent()]);
upsertRoomStateEvents(room, [mkPredecessorEvent(OLD_ROOM_ID)]);

expect(getUnreadNotificationCount(room, NotificationCountType.Total)).toBe(8);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight)).toBe(7);
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(8);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, false)).toBe(7);
});

it("and there is an unknown room in the predecessor event, it should not count predecessor highlight", () => {
room.addLiveEvents([mkCreateEvent()]);
upsertRoomStateEvents(room, [mkPredecessorEvent("!unknon:example.com")]);

expect(getUnreadNotificationCount(room, NotificationCountType.Total)).toBe(2);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight)).toBe(1);
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(2);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, false)).toBe(1);
});
});
});

it("counts thread notification type", () => {
expect(getUnreadNotificationCount(room, NotificationCountType.Total, THREAD_ID)).toBe(0);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, THREAD_ID)).toBe(0);
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false, THREAD_ID)).toBe(0);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, false, THREAD_ID)).toBe(0);
});

it("counts thread notifications type", () => {
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 2);
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 1);

expect(getUnreadNotificationCount(room, NotificationCountType.Total, THREAD_ID)).toBe(2);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, THREAD_ID)).toBe(1);
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false, THREAD_ID)).toBe(2);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, false, THREAD_ID)).toBe(1);
});
});

Expand Down
Loading

0 comments on commit 77e1649

Please sign in to comment.