// gomuks - A Matrix client written in Go. // Copyright (C) 2024 Tulir Asokan // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import { RoomListEntry, StateStore } from "@/api/statestore/main.ts" import { DBSpaceEdge, RoomID } from "@/api/types" import { NonNullCachedEventDispatcher } from "@/util/eventdispatcher.ts" export interface RoomListFilter { id: string include(room: RoomListEntry): boolean } export interface SpaceUnreadCounts { unread_messages: number unread_notifications: number unread_highlights: number } const emptyUnreadCounts: SpaceUnreadCounts = { unread_messages: 0, unread_notifications: 0, unread_highlights: 0, } export abstract class Space implements RoomListFilter { counts = new NonNullCachedEventDispatcher(emptyUnreadCounts) abstract id: string abstract include(room: RoomListEntry): boolean clearUnreads() { this.counts.emit(emptyUnreadCounts) } applyUnreads(newCounts?: SpaceUnreadCounts | null, oldCounts?: SpaceUnreadCounts | null) { const mergedCounts: SpaceUnreadCounts = { unread_messages: this.counts.current.unread_messages + (newCounts?.unread_messages ?? 0) - (oldCounts?.unread_messages ?? 0), unread_notifications: this.counts.current.unread_notifications + (newCounts?.unread_notifications ?? 0) - (oldCounts?.unread_notifications ?? 0), unread_highlights: this.counts.current.unread_highlights + (newCounts?.unread_highlights ?? 0) - (oldCounts?.unread_highlights ?? 0), } if (mergedCounts.unread_messages < 0) { mergedCounts.unread_messages = 0 } if (mergedCounts.unread_notifications < 0) { mergedCounts.unread_notifications = 0 } if (mergedCounts.unread_highlights < 0) { mergedCounts.unread_highlights = 0 } if ( mergedCounts.unread_messages !== this.counts.current.unread_messages || mergedCounts.unread_notifications !== this.counts.current.unread_notifications || mergedCounts.unread_highlights !== this.counts.current.unread_highlights ) { this.counts.emit(mergedCounts) } } } export class DirectChatSpace extends Space { id = "fi.mau.gomuks.direct_chats" include(room: RoomListEntry): boolean { return Boolean(room.dm_user_id) } } export class UnreadsSpace extends Space { id = "fi.mau.gomuks.unreads" constructor(private parent: StateStore) { super() } include(room: RoomListEntry): boolean { return Boolean(room.room_id === this.parent.activeRoomID || room.unread_messages || room.unread_notifications || room.unread_highlights || room.marked_unread) } } export class SpaceEdgeStore extends Space { #children: DBSpaceEdge[] = [] #childRooms: Set = new Set() #flattenedRooms: Set = new Set() #childSpaces: Set = new Set() readonly #parentSpaces: Set = new Set() constructor(public id: RoomID, private parent: StateStore) { super() } #addParent(parent: SpaceEdgeStore) { this.#parentSpaces.add(parent) } #removeParent(parent: SpaceEdgeStore) { this.#parentSpaces.delete(parent) } include(room: RoomListEntry) { return this.#flattenedRooms.has(room.room_id) } get children() { return this.#children } #updateFlattened(recalculate: boolean, added: Set) { if (recalculate) { let flattened = new Set(this.#childRooms) for (const space of this.#childSpaces) { flattened = flattened.union(space.#flattenedRooms) } this.#flattenedRooms = flattened } else if (added.size > 50) { this.#flattenedRooms = this.#flattenedRooms.union(added) } else if (added.size > 0) { for (const room of added) { this.#flattenedRooms.add(room) } } } #notifyParentsOfChange(recalculate: boolean, added: Set, stack: WeakSet) { if (stack.has(this)) { return } stack.add(this) for (const parent of this.#parentSpaces) { parent.#updateFlattened(recalculate, added) parent.#notifyParentsOfChange(recalculate, added, stack) } stack.delete(this) } set children(newChildren: DBSpaceEdge[]) { const newChildRooms = new Set() const newChildSpaces = new Set() for (const child of newChildren) { const spaceStore = this.parent.getSpaceStore(child.child_id) if (spaceStore) { newChildSpaces.add(spaceStore) spaceStore.#addParent(this) } else { newChildRooms.add(child.child_id) } } for (const space of this.#childSpaces) { if (!newChildSpaces.has(space)) { space.#removeParent(this) } } const addedRooms = newChildRooms.difference(this.#childRooms) const removedRooms = this.#childRooms.difference(newChildRooms) const didAddChildren = newChildSpaces.difference(this.#childSpaces).size > 0 const recalculateFlattened = removedRooms.size > 0 || didAddChildren this.#children = newChildren this.#childRooms = newChildRooms this.#childSpaces = newChildSpaces if (this.#childSpaces.size > 0) { this.#updateFlattened(recalculateFlattened, addedRooms) } else { this.#flattenedRooms = newChildRooms } if (this.#parentSpaces.size > 0) { this.#notifyParentsOfChange(recalculateFlattened, addedRooms, new WeakSet()) } } } export class SpaceOrphansSpace extends SpaceEdgeStore { static id = "fi.mau.gomuks.space_orphans" constructor(parent: StateStore) { super(SpaceOrphansSpace.id, parent) } include(room: RoomListEntry): boolean { return !super.include(room) && !room.dm_user_id } }