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
}