// 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 unhomoglyph from "unhomoglyph" import { NonNullCachedEventDispatcher } from "@/util/eventdispatcher.ts" import type { ContentURI, EventsDecryptedData, MemDBEvent, RoomID, SendCompleteData, SyncCompleteData, SyncRoom, } from "../types" import { RoomStateStore } from "./room.ts" export interface RoomListEntry { room_id: RoomID sorting_timestamp: number preview_event?: MemDBEvent preview_sender?: MemDBEvent name: string search_name: string avatar?: ContentURI } // eslint-disable-next-line no-misleading-character-class const removeHiddenCharsRegex = /[\u2000-\u200F\u202A-\u202F\u0300-\u036F\uFEFF\u061C\u2800\u2062-\u2063\s]/g export function toSearchableString(str: string): string { return unhomoglyph(str.normalize("NFD").replace(removeHiddenCharsRegex, "")) .toLowerCase() .replace(/[\\'!"#$%&()*+,\-./:;<=>?@[\]^_`{|}~\u2000-\u206f\u2e00-\u2e7f]/g, "") .toLowerCase() } export class StateStore { readonly rooms: Map = new Map() readonly roomList = new NonNullCachedEventDispatcher([]) #roomListEntryChanged(entry: SyncRoom, oldEntry: RoomStateStore): boolean { return entry.meta.sorting_timestamp !== oldEntry.meta.current.sorting_timestamp || entry.meta.preview_event_rowid !== oldEntry.meta.current.preview_event_rowid || entry.events.findIndex(evt => evt.rowid === entry.meta.preview_event_rowid) !== -1 } #makeRoomListEntry(entry: SyncRoom, room?: RoomStateStore): RoomListEntry { if (!room) { room = this.rooms.get(entry.meta.room_id) } const preview_event = room?.eventsByRowID.get(entry.meta.preview_event_rowid) const preview_sender = preview_event && room?.getStateEvent("m.room.member", preview_event.sender) const name = entry.meta.name ?? "Unnamed room" return { room_id: entry.meta.room_id, sorting_timestamp: entry.meta.sorting_timestamp, preview_event, preview_sender, name, search_name: toSearchableString(name), avatar: entry.meta.avatar, } } applySync(sync: SyncCompleteData) { const resyncRoomList = this.roomList.current.length === 0 const changedRoomListEntries = new Map() for (const [roomID, data] of Object.entries(sync.rooms)) { let isNewRoom = false let room = this.rooms.get(roomID) if (!room) { room = new RoomStateStore(data.meta) this.rooms.set(roomID, room) isNewRoom = true } const roomListEntryChanged = !resyncRoomList && (isNewRoom || this.#roomListEntryChanged(data, room)) room.applySync(data) if (roomListEntryChanged) { changedRoomListEntries.set(roomID, this.#makeRoomListEntry(data, room)) } } let updatedRoomList: RoomListEntry[] | undefined if (resyncRoomList) { updatedRoomList = Object.values(sync.rooms).map(entry => this.#makeRoomListEntry(entry)) updatedRoomList.sort((r1, r2) => r1.sorting_timestamp - r2.sorting_timestamp) } else if (changedRoomListEntries.size > 0) { updatedRoomList = this.roomList.current.filter(entry => !changedRoomListEntries.has(entry.room_id)) for (const entry of changedRoomListEntries.values()) { if (updatedRoomList.length === 0 || entry.sorting_timestamp >= updatedRoomList[updatedRoomList.length - 1].sorting_timestamp) { updatedRoomList.push(entry) } else if (entry.sorting_timestamp <= 0 || entry.sorting_timestamp < updatedRoomList[0]?.sorting_timestamp) { updatedRoomList.unshift(entry) } else { const indexToPushAt = updatedRoomList.findLastIndex(val => val.sorting_timestamp <= entry.sorting_timestamp) updatedRoomList.splice(indexToPushAt + 1, 0, entry) } } } if (updatedRoomList) { this.roomList.emit(updatedRoomList) } } applySendComplete(data: SendCompleteData) { const room = this.rooms.get(data.event.room_id) if (!room) { // TODO log or something? return } room.applySendComplete(data.event) } applyDecrypted(decrypted: EventsDecryptedData) { const room = this.rooms.get(decrypted.room_id) if (!room) { // TODO log or something? return } room.applyDecrypted(decrypted) if (decrypted.preview_event_rowid) { const idx = this.roomList.current.findIndex(entry => entry.room_id === decrypted.room_id) if (idx !== -1) { const updatedRoomList = [...this.roomList.current] updatedRoomList[idx] = { ...updatedRoomList[idx], preview_event: room.eventsByRowID.get(decrypted.preview_event_rowid), } this.roomList.emit(updatedRoomList) } } } }