diff --git a/web/src/api/statestore/hooks.ts b/web/src/api/statestore/hooks.ts index 76ddce4..134d495 100644 --- a/web/src/api/statestore/hooks.ts +++ b/web/src/api/statestore/hooks.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 . import { useSyncExternalStore } from "react" -import type { EventID, MemDBEvent } from "../types" +import type { EventID, EventType, MemDBEvent } from "../types" import { RoomStateStore } from "./room.ts" export function useRoomTimeline(room: RoomStateStore): (MemDBEvent | null)[] { @@ -24,6 +24,15 @@ export function useRoomTimeline(room: RoomStateStore): (MemDBEvent | null)[] { ) } +export function useRoomState( + room: RoomStateStore, type: EventType, stateKey: string | undefined = "", +): MemDBEvent | null { + return useSyncExternalStore( + stateKey === undefined ? noopSubscribe : room.getStateSubscriber(type, stateKey).subscribe, + stateKey === undefined ? returnNull : (() => room.getStateEvent(type, stateKey) ?? null), + ) +} + const noopSubscribe = () => () => {} const returnNull = () => null diff --git a/web/src/api/statestore/room.ts b/web/src/api/statestore/room.ts index 482bd14..b1b073c 100644 --- a/web/src/api/statestore/room.ts +++ b/web/src/api/statestore/room.ts @@ -75,6 +75,7 @@ export class RoomStateStore { readonly eventsByRowID: Map = new Map() readonly eventsByID: Map = new Map() readonly timelineSub = new Subscribable() + readonly stateSubs: Map = new Map() readonly eventSubs: Map = new Map() readonly openNotifications: Map = new Map() readonly pendingEvents: EventRowID[] = [] @@ -110,6 +111,21 @@ export class RoomStateStore { return sub } + notifyStateSubscribers(eventType: EventType, stateKey: string) { + const subKey = `${eventType}:${stateKey}` + this.stateSubs.get(subKey)?.notify() + } + + getStateSubscriber(eventType: EventType, stateKey: string): Subscribable { + const subKey = `${eventType}:${stateKey}` + let sub = this.stateSubs.get(subKey) + if (!sub) { + sub = new Subscribable(() => this.stateSubs.delete(subKey)) + this.stateSubs.set(subKey, sub) + } + return sub + } + getStateEvent(type: EventType, stateKey: string): MemDBEvent | undefined { const rowID = this.state.get(type)?.get(stateKey) if (!rowID) { @@ -200,6 +216,7 @@ export class RoomStateStore { } for (const [key, rowID] of Object.entries(changedEvts)) { stateMap.set(key, rowID) + this.notifyStateSubscribers(evtType, key) } } if (sync.reset) { diff --git a/web/src/ui/timeline/ReplyBody.tsx b/web/src/ui/timeline/ReplyBody.tsx index 455b635..3534845 100644 --- a/web/src/ui/timeline/ReplyBody.tsx +++ b/web/src/ui/timeline/ReplyBody.tsx @@ -15,7 +15,7 @@ // along with this program. If not, see . import { use } from "react" import { getAvatarURL } from "@/api/media.ts" -import { RoomStateStore, useRoomEvent } from "@/api/statestore" +import { RoomStateStore, useRoomEvent, useRoomState } from "@/api/statestore" import type { EventID, MemDBEvent, MemberEventContent } from "@/api/types" import { ClientContext } from "../ClientContext.ts" import getBodyType, { ContentErrorBoundary } from "./content" @@ -63,7 +63,7 @@ const onClickReply = (evt: React.MouseEvent) => { } export const ReplyBody = ({ room, event, onClose }: ReplyBodyProps) => { - const memberEvt = room.getStateEvent("m.room.member", event.sender) + const memberEvt = useRoomState(room, "m.room.member", event.sender) const memberEvtContent = memberEvt?.content as MemberEventContent | undefined const BodyType = getBodyType(event, true) return
{ {onClose && } - +
} diff --git a/web/src/ui/timeline/TimelineEvent.tsx b/web/src/ui/timeline/TimelineEvent.tsx index 01238dc..93a84d9 100644 --- a/web/src/ui/timeline/TimelineEvent.tsx +++ b/web/src/ui/timeline/TimelineEvent.tsx @@ -15,7 +15,7 @@ // along with this program. If not, see . import React, { use, useCallback } from "react" import { getAvatarURL } from "@/api/media.ts" -import { RoomStateStore } from "@/api/statestore" +import { RoomStateStore, useRoomState } from "@/api/statestore" import { EventID, MemDBEvent, MemberEventContent, UnreadType } from "@/api/types" import { isEventID } from "@/util/validation.ts" import { ClientContext } from "../ClientContext.ts" @@ -66,7 +66,7 @@ function isSmallEvent(bodyType: React.FunctionComponent): boo const TimelineEvent = ({ room, evt, prevEvt, setReplyToRef }: TimelineEventProps) => { const wrappedSetReplyTo = useCallback(() => setReplyToRef.current(evt.event_id), [evt, setReplyToRef]) const client = use(ClientContext)! - const memberEvt = room.getStateEvent("m.room.member", evt.sender) + const memberEvt = useRoomState(room, "m.room.member", evt.sender) const memberEvtContent = memberEvt?.content as MemberEventContent | undefined const BodyType = getBodyType(evt) const eventTS = new Date(evt.timestamp) diff --git a/web/src/ui/timeline/content/MediaMessageBody.tsx b/web/src/ui/timeline/content/MediaMessageBody.tsx index 4fd6b8d..712f320 100644 --- a/web/src/ui/timeline/content/MediaMessageBody.tsx +++ b/web/src/ui/timeline/content/MediaMessageBody.tsx @@ -18,11 +18,11 @@ import TextMessageBody from "./TextMessageBody.tsx" import EventContentProps from "./props.ts" import { useMediaContent } from "./useMediaContent.tsx" -const MediaMessageBody = ({ event, room }: EventContentProps) => { +const MediaMessageBody = ({ event, room, sender }: EventContentProps) => { const content = event.content as MediaMessageEventContent let caption = null if (content.body && content.filename && content.body !== content.filename) { - caption = + caption = } const [mediaContent, containerClass, containerStyle] = useMediaContent(content, event.type) return <> diff --git a/web/src/ui/timeline/content/TextMessageBody.tsx b/web/src/ui/timeline/content/TextMessageBody.tsx index 5eb8f8f..a3cc5b3 100644 --- a/web/src/ui/timeline/content/TextMessageBody.tsx +++ b/web/src/ui/timeline/content/TextMessageBody.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { MemberEventContent, MessageEventContent } from "@/api/types" +import { MessageEventContent } from "@/api/types" import EventContentProps from "./props.ts" const onClickHTML = (evt: React.MouseEvent) => { @@ -23,7 +23,7 @@ const onClickHTML = (evt: React.MouseEvent) => { } } -const TextMessageBody = ({ event, room }: EventContentProps) => { +const TextMessageBody = ({ event, sender }: EventContentProps) => { const content = event.content as MessageEventContent const classNames = ["message-text"] let eventSenderName: string | undefined @@ -31,9 +31,7 @@ const TextMessageBody = ({ event, room }: EventContentProps) => { classNames.push("notice-message") } else if (content.msgtype === "m.emote") { classNames.push("emote-message") - const memberEvt = room.getStateEvent("m.room.member", event.sender) - const memberEvtContent = memberEvt?.content as MemberEventContent | undefined - eventSenderName = memberEvtContent?.displayname || event.sender + eventSenderName = sender?.content?.displayname || event.sender } if (event.local_content?.sanitized_html) { classNames.push("html-body") diff --git a/web/src/ui/timeline/content/props.ts b/web/src/ui/timeline/content/props.ts index 93da6cb..900265e 100644 --- a/web/src/ui/timeline/content/props.ts +++ b/web/src/ui/timeline/content/props.ts @@ -19,5 +19,5 @@ import { MemDBEvent } from "@/api/types" export default interface EventContentProps { room: RoomStateStore event: MemDBEvent - sender?: MemDBEvent + sender: MemDBEvent | null }