forked from Mirrors/gomuks
web/statestore: split into multiple files
This commit is contained in:
parent
2fc1aff753
commit
bbc59a2f89
13 changed files with 190 additions and 147 deletions
|
@ -15,7 +15,7 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { CachedEventDispatcher } from "../util/eventdispatcher.ts"
|
||||
import RPCClient, { SendMessageParams } from "./rpc.ts"
|
||||
import { StateStore } from "./statestore.ts"
|
||||
import { StateStore } from "./statestore"
|
||||
import type {
|
||||
ClientState,
|
||||
EventRowID,
|
||||
|
|
25
web/src/api/statestore/hooks.ts
Normal file
25
web/src/api/statestore/hooks.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
// 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 <https://www.gnu.org/licenses/>.
|
||||
import { useSyncExternalStore } from "react"
|
||||
import type { MemDBEvent } from "../types"
|
||||
import { RoomStateStore } from "./room.ts"
|
||||
|
||||
export function useRoomTimeline(room: RoomStateStore): (MemDBEvent | null)[] {
|
||||
return useSyncExternalStore(
|
||||
room.subscribeTimeline,
|
||||
() => room.timelineCache,
|
||||
)
|
||||
}
|
3
web/src/api/statestore/index.ts
Normal file
3
web/src/api/statestore/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from "./main.ts"
|
||||
export * from "./room.ts"
|
||||
export * from "./hooks.ts"
|
148
web/src/api/statestore/main.ts
Normal file
148
web/src/api/statestore/main.ts
Normal file
|
@ -0,0 +1,148 @@
|
|||
// 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 <https://www.gnu.org/licenses/>.
|
||||
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<RoomID, RoomStateStore> = new Map()
|
||||
readonly roomList = new NonNullCachedEventDispatcher<RoomListEntry[]>([])
|
||||
|
||||
#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<RoomID, RoomListEntry>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,11 +13,8 @@
|
|||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { useSyncExternalStore } from "react"
|
||||
import unhomoglyph from "unhomoglyph"
|
||||
import { NonNullCachedEventDispatcher } from "../util/eventdispatcher.ts"
|
||||
import { NonNullCachedEventDispatcher } from "@/util/eventdispatcher.ts"
|
||||
import type {
|
||||
ContentURI,
|
||||
DBRoom,
|
||||
EncryptedEventContent,
|
||||
EventID,
|
||||
|
@ -28,11 +25,9 @@ import type {
|
|||
MemDBEvent,
|
||||
RawDBEvent,
|
||||
RoomID,
|
||||
SendCompleteData,
|
||||
SyncCompleteData,
|
||||
SyncRoom,
|
||||
TimelineRowTuple,
|
||||
} from "./types"
|
||||
} from "../types"
|
||||
|
||||
function arraysAreEqual<T>(arr1?: T[], arr2?: T[]): boolean {
|
||||
if (!arr1 || !arr2) {
|
||||
|
@ -65,13 +60,6 @@ function visibleMetaIsEqual(meta1: DBRoom, meta2: DBRoom): boolean {
|
|||
meta1.has_member_list === meta2.has_member_list
|
||||
}
|
||||
|
||||
export function useRoomTimeline(room: RoomStateStore): (MemDBEvent | null)[] {
|
||||
return useSyncExternalStore(
|
||||
room.subscribeTimeline,
|
||||
() => room.timelineCache,
|
||||
)
|
||||
}
|
||||
|
||||
type SubscribeFunc = (callback: () => void) => () => void
|
||||
|
||||
export class RoomStateStore {
|
||||
|
@ -214,124 +202,3 @@ export class RoomStateStore {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<RoomID, RoomStateStore> = new Map()
|
||||
readonly roomList = new NonNullCachedEventDispatcher<RoomListEntry[]>([])
|
||||
|
||||
#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<RoomID, RoomListEntry>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import React, { use, useCallback, useRef, useState } from "react"
|
||||
import { RoomStateStore } from "@/api/statestore.ts"
|
||||
import { RoomStateStore } from "@/api/statestore"
|
||||
import { MemDBEvent, Mentions } from "@/api/types"
|
||||
import { ClientContext } from "./ClientContext.ts"
|
||||
import ReplyBody from "./timeline/ReplyBody.tsx"
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { use, useCallback, useState } from "react"
|
||||
import { getMediaURL } from "@/api/media.ts"
|
||||
import { RoomStateStore } from "@/api/statestore.ts"
|
||||
import { RoomStateStore } from "@/api/statestore"
|
||||
import { MemDBEvent } from "@/api/types"
|
||||
import { useNonNullEventAsState } from "@/util/eventdispatcher.ts"
|
||||
import { LightboxContext } from "./Lightbox.tsx"
|
||||
|
|
|
@ -14,9 +14,9 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { use } from "react"
|
||||
import { getMediaURL } from "../../api/media.ts"
|
||||
import type { RoomListEntry } from "../../api/statestore.ts"
|
||||
import type { MemDBEvent, MemberEventContent } from "../../api/types"
|
||||
import { getMediaURL } from "@/api/media.ts"
|
||||
import type { RoomListEntry } from "@/api/statestore"
|
||||
import type { MemDBEvent, MemberEventContent } from "@/api/types"
|
||||
import { ClientContext } from "../ClientContext.ts"
|
||||
|
||||
export interface RoomListEntryProps {
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import React, { use, useCallback, useRef, useState } from "react"
|
||||
import { toSearchableString } from "@/api/statestore.ts"
|
||||
import { toSearchableString } from "@/api/statestore"
|
||||
import type { RoomID } from "@/api/types"
|
||||
import { useNonNullEventAsState } from "@/util/eventdispatcher.ts"
|
||||
import { ClientContext } from "../ClientContext.ts"
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { getAvatarURL } from "@/api/media.ts"
|
||||
import type { RoomStateStore } from "@/api/statestore.ts"
|
||||
import type { RoomStateStore } from "@/api/statestore"
|
||||
import type { EventID, MemDBEvent, MemberEventContent } from "@/api/types"
|
||||
import { TextMessageBody } from "./content/MessageBody.tsx"
|
||||
import CloseButton from "@/icons/close.svg?react"
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import React, { use, useCallback } from "react"
|
||||
import { getAvatarURL } from "@/api/media.ts"
|
||||
import { RoomStateStore } from "@/api/statestore.ts"
|
||||
import { RoomStateStore } from "@/api/statestore"
|
||||
import { MemDBEvent, MemberEventContent } from "@/api/types"
|
||||
import { ClientContext } from "../ClientContext.ts"
|
||||
import { LightboxContext } from "../Lightbox.tsx"
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { use, useCallback, useEffect, useRef } from "react"
|
||||
import { RoomStateStore, useRoomTimeline } from "@/api/statestore.ts"
|
||||
import { RoomStateStore, useRoomTimeline } from "@/api/statestore"
|
||||
import { MemDBEvent } from "@/api/types"
|
||||
import { ClientContext } from "../ClientContext.ts"
|
||||
import TimelineEvent from "./TimelineEvent.tsx"
|
||||
|
|
|
@ -13,8 +13,8 @@
|
|||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { RoomStateStore } from "../../../api/statestore.ts"
|
||||
import { MemDBEvent } from "../../../api/types"
|
||||
import { RoomStateStore } from "@/api/statestore"
|
||||
import { MemDBEvent } from "@/api/types"
|
||||
|
||||
export interface EventContentProps {
|
||||
room: RoomStateStore
|
||||
|
|
Loading…
Add table
Reference in a new issue