153 lines
5.6 KiB
TypeScript
153 lines
5.6 KiB
TypeScript
/*
|
|
Copyright 2024 New Vector Ltd.
|
|
Copyright 2015-2023 The Matrix.org Foundation C.I.C.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
Please see LICENSE files in the repository root for full details.
|
|
*/
|
|
|
|
import { M_BEACON, Room, Thread, MatrixEvent, EventType, MatrixClient } from "matrix-js-sdk/src/matrix";
|
|
import { logger } from "matrix-js-sdk/src/logger";
|
|
|
|
import shouldHideEvent from "./shouldHideEvent";
|
|
import { haveRendererForEvent } from "./events/EventTileFactory";
|
|
import SettingsStore from "./settings/SettingsStore";
|
|
import { RoomNotifState, getRoomNotifsState } from "./RoomNotifs";
|
|
|
|
/**
|
|
* Returns true if this event arriving in a room should affect the room's
|
|
* count of unread messages
|
|
*
|
|
* @param client The Matrix Client instance of the logged-in user
|
|
* @param {Object} ev The event
|
|
* @returns {boolean} True if the given event should affect the unread message count
|
|
*/
|
|
export function eventTriggersUnreadCount(client: MatrixClient, ev: MatrixEvent): boolean {
|
|
if (ev.getSender() === client.getSafeUserId()) {
|
|
return false;
|
|
}
|
|
|
|
switch (ev.getType()) {
|
|
case EventType.RoomMember:
|
|
case EventType.RoomThirdPartyInvite:
|
|
case EventType.CallAnswer:
|
|
case EventType.CallHangup:
|
|
case EventType.RoomCanonicalAlias:
|
|
case EventType.RoomServerAcl:
|
|
case M_BEACON.name:
|
|
case M_BEACON.altName:
|
|
return false;
|
|
}
|
|
|
|
if (ev.isRedacted()) return false;
|
|
return haveRendererForEvent(ev, client, false /* hidden messages should never trigger unread counts anyways */);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// If we got here then no timelines were found with unread messages.
|
|
return false;
|
|
}
|
|
|
|
function doesTimelineHaveUnreadMessages(room: Room, timeline: Array<MatrixEvent>): boolean {
|
|
// The room is a space, let's ignore it
|
|
if (room.isSpaceRoom()) return false;
|
|
|
|
const myUserId = room.client.getSafeUserId();
|
|
const latestImportantEventId = findLatestImportantEvent(room.client, timeline)?.getId();
|
|
if (latestImportantEventId) {
|
|
return !room.hasUserReadEvent(myUserId, latestImportantEventId);
|
|
} else {
|
|
// We couldn't find an important event to check - check the unimportant ones.
|
|
const earliestUnimportantEventId = timeline.at(0)?.getId();
|
|
if (!earliestUnimportantEventId) {
|
|
// There are no events in this timeline - it is uninitialised, so we
|
|
// consider it read
|
|
return false;
|
|
} else if (room.hasUserReadEvent(myUserId, earliestUnimportantEventId)) {
|
|
// Some of the unimportant events are read, and there are no
|
|
// important ones after them, so we've read everything.
|
|
return false;
|
|
} else {
|
|
// We have events. and none of them are read. We must guess that
|
|
// the timeline is unread, because there could be older unread
|
|
// important events that we don't have loaded.
|
|
logger.warn("Falling back to unread room because of no read receipt or counting message found", {
|
|
roomId: room.roomId,
|
|
earliestUnimportantEventId: earliestUnimportantEventId,
|
|
});
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if this room has unread threads.
|
|
* @param room The room to check
|
|
* @returns {boolean} True if the given room has unread threads
|
|
*/
|
|
export function doesRoomHaveUnreadThreads(room: Room): boolean {
|
|
if (getRoomNotifsState(room.client, room.roomId) === RoomNotifState.Mute) {
|
|
// No unread for muted rooms, nor their threads
|
|
// NB. This logic duplicated in RoomNotifs.determineUnreadState
|
|
return false;
|
|
}
|
|
|
|
for (const thread of room.getThreads()) {
|
|
if (doesTimelineHaveUnreadMessages(room, thread.timeline)) {
|
|
// We found an unread, so the room has an unread thread
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// If we got here then no threads were found with unread messages.
|
|
return false;
|
|
}
|
|
|
|
export function doesRoomOrThreadHaveUnreadMessages(roomOrThread: Room | Thread): boolean {
|
|
const room = roomOrThread instanceof Thread ? roomOrThread.room : roomOrThread;
|
|
const events = roomOrThread instanceof Thread ? roomOrThread.timeline : room.getLiveTimeline().getEvents();
|
|
return doesTimelineHaveUnreadMessages(room, events);
|
|
}
|
|
|
|
/**
|
|
* Look backwards through the timeline and find the last event that is
|
|
* "important" in the sense of isImportantEvent.
|
|
*
|
|
* @returns the latest important event, or null if none were found
|
|
*/
|
|
function findLatestImportantEvent(client: MatrixClient, timeline: Array<MatrixEvent>): MatrixEvent | null {
|
|
for (let index = timeline.length - 1; index >= 0; index--) {
|
|
const event = timeline[index];
|
|
if (isImportantEvent(client, event)) {
|
|
return event;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Given this event does not have a receipt, is it important enough to make
|
|
* this room unread?
|
|
*/
|
|
function isImportantEvent(client: MatrixClient, event: MatrixEvent): boolean {
|
|
return !shouldHideEvent(event) && eventTriggersUnreadCount(client, event);
|
|
}
|