/* Copyright 2015 - 2023 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 { Room } from "matrix-js-sdk/src/models/room"; import { Thread } from "matrix-js-sdk/src/models/thread"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { EventType } from "matrix-js-sdk/src/@types/event"; import { M_BEACON } from "matrix-js-sdk/src/@types/beacon"; import { MatrixClientPeg } from "./MatrixClientPeg"; import shouldHideEvent from "./shouldHideEvent"; import { haveRendererForEvent } from "./events/EventTileFactory"; import SettingsStore from "./settings/SettingsStore"; /** * Returns true if this event arriving in a room should affect the room's * count of unread messages * * @param {Object} ev The event * @returns {boolean} True if the given event should affect the unread message count */ export function eventTriggersUnreadCount(ev: MatrixEvent): boolean { if (ev.getSender() === MatrixClientPeg.get().credentials.userId) { 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, false /* hidden messages should never trigger unread counts anyways */); } export function doesRoomHaveUnreadMessages(room: Room): 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 timeline of [room, ...room.getThreads()]) { // If the current timeline has unread messages, we're done. if (doesRoomOrThreadHaveUnreadMessages(timeline)) { return true; } } // If we got here then no timelines were found with unread messages. return false; } export function doesRoomOrThreadHaveUnreadMessages(roomOrThread: Room | Thread): boolean { // If there are no messages yet in the timeline then it isn't fully initialised // and cannot be unread. if (!roomOrThread || roomOrThread.timeline.length === 0) { return false; } const myUserId = MatrixClientPeg.get().getUserId(); // as we don't send RRs for our own messages, make sure we special case that // if *we* sent the last message into the room, we consider it not unread! // Should fix: https://github.com/vector-im/element-web/issues/3263 // https://github.com/vector-im/element-web/issues/2427 // ...and possibly some of the others at // https://github.com/vector-im/element-web/issues/3363 if (roomOrThread.timeline[roomOrThread.timeline.length - 1]?.getSender() === myUserId) { return false; } // get the most recent read receipt sent by our account. // N.B. this is NOT a read marker (RM, aka "read up to marker"), // despite the name of the method :(( const readUpToId = roomOrThread.getEventReadUpTo(myUserId!); // this just looks at whatever history we have, which if we've only just started // up probably won't be very much, so if the last couple of events are ones that // don't count, we don't know if there are any events that do count between where // we have and the read receipt. We could fetch more history to try & find out, // but currently we just guess. // Loop through messages, starting with the most recent... for (let i = roomOrThread.timeline.length - 1; i >= 0; --i) { const ev = roomOrThread.timeline[i]; if (ev.getId() == readUpToId) { // If we've read up to this event, there's nothing more recent // that counts and we can stop looking because the user's read // this and everything before. return false; } else if (!shouldHideEvent(ev) && eventTriggersUnreadCount(ev)) { // We've found a message that counts before we hit // the user's read receipt, so this room is definitely unread. return true; } } // If we got here, we didn't find a message that counted but didn't find // the user's read receipt either, so we guess and say that the room is // unread on the theory that false positives are better than false // negatives here. return true; }