a5ed97b903
* Support the mark as unread flag * Add mark as unread menu option and make clering notifications also clear the unread flag * Mark as read on viewing room * Tests * Remove random import * Don't show mark as unread for historical rooms * Fix tests & add test for menu option * Test RoomNotificationState updates on unread flag change * Test it doesn't update on other room account data * New icon for mark as unread * Add analytics events for mark as (un)read * Bump to new analytics-events package * Read from both stable & unstable prefixes * Cast to boolean before checking to avoid setting state unnecessarily * Typo Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Doc external interface (and the rest at the same time) * Doc & rename unread market set function * Doc const exports * Remove listener on destroy * Add playwright test * Clearer language, hopefully * Move comment * Add reference to the MSC Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Expand on function doc * Remove empty beforeEach * Rejig badge logic a little and add tests * Fix basdges to not display dots in room sublists again and hopefully rename the forceDot option to something that better indicates what it does, and add tests. * Remove duplicate license header (?) * Missing word (several times...) * Incorporate PR suggestion on badge type switch * Better description in doc comment Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Update other doc comments in the same way * Remove duplicate quote * Use quotes consistently * Better test name * c+p fail --------- Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
202 lines
7.6 KiB
TypeScript
202 lines
7.6 KiB
TypeScript
/*
|
|
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 {
|
|
MatrixClient,
|
|
LOCAL_NOTIFICATION_SETTINGS_PREFIX,
|
|
NotificationCountType,
|
|
Room,
|
|
LocalNotificationSettings,
|
|
ReceiptType,
|
|
IMarkedUnreadEvent,
|
|
} from "matrix-js-sdk/src/matrix";
|
|
import { IndicatorIcon } from "@vector-im/compound-web";
|
|
|
|
import SettingsStore from "../settings/SettingsStore";
|
|
import { NotificationLevel } from "../stores/notifications/NotificationLevel";
|
|
import { doesRoomHaveUnreadMessages } from "../Unread";
|
|
|
|
// MSC2867 is not yet spec at time of writing. We read from both stable
|
|
// and unstable prefixes and accept the risk that the format may change,
|
|
// since the stable prefix is not actually defined yet.
|
|
|
|
/**
|
|
* Unstable identifier for the marked_unread event, per MSC2867
|
|
*/
|
|
export const MARKED_UNREAD_TYPE_UNSTABLE = "com.famedly.marked_unread";
|
|
/**
|
|
* Stable identifier for the marked_unread event
|
|
*/
|
|
export const MARKED_UNREAD_TYPE_STABLE = "m.marked_unread";
|
|
|
|
export const deviceNotificationSettingsKeys = [
|
|
"notificationsEnabled",
|
|
"notificationBodyEnabled",
|
|
"audioNotificationsEnabled",
|
|
];
|
|
|
|
export function getLocalNotificationAccountDataEventType(deviceId: string | null): string {
|
|
return `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`;
|
|
}
|
|
|
|
export async function createLocalNotificationSettingsIfNeeded(cli: MatrixClient): Promise<void> {
|
|
if (cli.isGuest()) {
|
|
return;
|
|
}
|
|
const eventType = getLocalNotificationAccountDataEventType(cli.deviceId!);
|
|
const event = cli.getAccountData(eventType);
|
|
// New sessions will create an account data event to signify they support
|
|
// remote toggling of push notifications on this device. Default `is_silenced=true`
|
|
// For backwards compat purposes, older sessions will need to check settings value
|
|
// to determine what the state of `is_silenced`
|
|
if (!event) {
|
|
// If any of the above is true, we fall in the "backwards compat" case,
|
|
// and `is_silenced` will be set to `false`
|
|
const isSilenced = !deviceNotificationSettingsKeys.some((key) => SettingsStore.getValue(key));
|
|
|
|
await cli.setAccountData(eventType, {
|
|
is_silenced: isSilenced,
|
|
});
|
|
}
|
|
}
|
|
|
|
export function localNotificationsAreSilenced(cli: MatrixClient): boolean {
|
|
const eventType = getLocalNotificationAccountDataEventType(cli.deviceId!);
|
|
const event = cli.getAccountData(eventType);
|
|
return event?.getContent<LocalNotificationSettings>()?.is_silenced ?? false;
|
|
}
|
|
|
|
/**
|
|
* Mark a room as read
|
|
* @param room
|
|
* @param client
|
|
* @returns a promise that resolves when the room has been marked as read
|
|
*/
|
|
export async function clearRoomNotification(room: Room, client: MatrixClient): Promise<{} | undefined> {
|
|
const lastEvent = room.getLastLiveEvent();
|
|
|
|
await setMarkedUnreadState(room, client, false);
|
|
|
|
try {
|
|
if (lastEvent) {
|
|
const receiptType = SettingsStore.getValue("sendReadReceipts", room.roomId)
|
|
? ReceiptType.Read
|
|
: ReceiptType.ReadPrivate;
|
|
return await client.sendReadReceipt(lastEvent, receiptType, true);
|
|
} else {
|
|
return {};
|
|
}
|
|
} finally {
|
|
// We've had a lot of stuck unread notifications that in e2ee rooms
|
|
// They occur on event decryption when clients try to replicate the logic
|
|
//
|
|
// This resets the notification on a room, even though no read receipt
|
|
// has been sent, particularly useful when the clients has incorrectly
|
|
// notified a user.
|
|
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
|
|
room.setUnreadNotificationCount(NotificationCountType.Total, 0);
|
|
for (const thread of room.getThreads()) {
|
|
room.setThreadUnreadNotificationCount(thread.id, NotificationCountType.Highlight, 0);
|
|
room.setThreadUnreadNotificationCount(thread.id, NotificationCountType.Total, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Marks all rooms with an unread counter as read
|
|
* @param client The matrix client
|
|
* @returns a promise that resolves when all rooms have been marked as read
|
|
*/
|
|
export function clearAllNotifications(client: MatrixClient): Promise<Array<{} | undefined>> {
|
|
const receiptPromises = client.getRooms().reduce((promises: Array<Promise<{} | undefined>>, room: Room) => {
|
|
if (doesRoomHaveUnreadMessages(room, true)) {
|
|
const promise = clearRoomNotification(room, client);
|
|
promises.push(promise);
|
|
}
|
|
|
|
return promises;
|
|
}, []);
|
|
|
|
return Promise.all(receiptPromises);
|
|
}
|
|
|
|
/**
|
|
* Gives the marked_unread state of the given room
|
|
* @param room The room to check
|
|
* @returns - The marked_unread state of the room, or undefined if no explicit state is set.
|
|
*/
|
|
export function getMarkedUnreadState(room: Room): boolean | undefined {
|
|
const currentStateStable = room.getAccountData(MARKED_UNREAD_TYPE_STABLE)?.getContent<IMarkedUnreadEvent>()?.unread;
|
|
const currentStateUnstable = room
|
|
.getAccountData(MARKED_UNREAD_TYPE_UNSTABLE)
|
|
?.getContent<IMarkedUnreadEvent>()?.unread;
|
|
return currentStateStable ?? currentStateUnstable;
|
|
}
|
|
|
|
/**
|
|
* Sets the marked_unread state of the given room. This sets some room account data that indicates to
|
|
* clients that the user considers this room to be 'unread', but without any actual notifications.
|
|
*
|
|
* @param room The room to set
|
|
* @param client MatrixClient object to use
|
|
* @param unread The new marked_unread state of the room
|
|
*/
|
|
export async function setMarkedUnreadState(room: Room, client: MatrixClient, unread: boolean): Promise<void> {
|
|
// if there's no event, treat this as false as we don't need to send the flag to clear it if the event isn't there
|
|
const currentState = getMarkedUnreadState(room);
|
|
|
|
if (Boolean(currentState) !== unread) {
|
|
// Assuming MSC2867 passes FCP with no changes, we should update to start writing
|
|
// the flag to the stable prefix (or both) and then ultimately use only the
|
|
// stable prefix.
|
|
await client.setRoomAccountData(room.roomId, MARKED_UNREAD_TYPE_UNSTABLE, { unread });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A helper to transform a notification color to the what the Compound Icon Button
|
|
* expects
|
|
*/
|
|
export function notificationLevelToIndicator(
|
|
level: NotificationLevel,
|
|
): React.ComponentPropsWithRef<typeof IndicatorIcon>["indicator"] {
|
|
if (level <= NotificationLevel.None) {
|
|
return undefined;
|
|
} else if (level <= NotificationLevel.Activity) {
|
|
return "default";
|
|
} else if (level <= NotificationLevel.Notification) {
|
|
return "success";
|
|
} else {
|
|
return "critical";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the thread notification level for a room
|
|
* @param room
|
|
* @returns {NotificationLevel}
|
|
*/
|
|
export function getThreadNotificationLevel(room: Room): NotificationLevel {
|
|
const notificationCountType = room.threadsAggregateNotificationType;
|
|
switch (notificationCountType) {
|
|
case NotificationCountType.Highlight:
|
|
return NotificationLevel.Highlight;
|
|
case NotificationCountType.Total:
|
|
return NotificationLevel.Notification;
|
|
default:
|
|
return NotificationLevel.Activity;
|
|
}
|
|
}
|