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

Add labs flag for Threads Activity Centre #12137

Merged
merged 19 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
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 @@ -1419,6 +1419,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 @@ -1462,6 +1463,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
Loading