* Add `Thread Activity centre` labs flag * Rename translation string * WIP Thread Activity Centre * Update supportedLevels * css lint * i18n lint * Fix labs subsection test * Update Threads Activity Centre label * Rename Thread Activity Centre to Threads Activity Centre * Use compound `MenuItem` instead of custom button * Color thread icon when hovered * Make the pop-up scrollable and add a max height * Remove Math.random in key * Remove unused class * Change add comments on `mx_ThreadsActivityRows` and `mx_ThreadsActivityRow` * 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 * Open thread panel when thread clicke in Threads Activity Centre Hopefully this is a sensible enough way. The panel will stay open of course (ie. if you go to a different room & come back), but that's the nature of the right panel. * Dynamic state of room * Add doc * Use the StatelessNotificationBadge component in ThreadsActivityCentre and re-use the existing NotificationLevel * Remove unused style * Add room sorting * Fix `ThreadsActivityRow` props doc * Pass in & cache the status of the TAC labs flag * Pass includeThreads as setting to doesRoomHaveUnreadMessages too * Fix tests * Add analytics to the TAC (#12179) * Update TAC label (#12186) * Add `IndicatorIcon` to the TAC button (#12182) Add `IndicatorIcon` to the TAC button * Threads don't have activity if the room is muted This makes it match the computation in determineUnreadState. Ideally this logic should all be in one place. * Re-use doesRoomHaveUnreadThreads for useRoomThreadNotifications This incorporates the logic of not showing unread dots if the room is muted * Add TAC description in labs (#12197) * Fox position & size of dot on the tac button IndicatorIcon doesn't like having the size of its icon adjusted and we probably shouldn't do it anyway: better to specify to the component what size we want it. * TAC: Utils tests (#12200) * Add tests for `doesRoomHaveUnreadThreads` * Add tests for `getThreadNotificationLevel` * Add test for the ThreadsActivityCentre component * Add snapshot test * Fix narrow hover background on TAC button Make the button 32x32 (and the inner icon 24x24) * Add caption for empty TAC * s/tac/threads_activity_centre/ * Fix i18n & add tests * Add playwright tests for the TAC (#12227) * Fox comments --------- Co-authored-by: David Baker <dbkr@users.noreply.github.com>
160 lines
6 KiB
TypeScript
160 lines
6 KiB
TypeScript
/*
|
|
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 { 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);
|
|
}
|