1
0
Fork 0
forked from Mirrors/gomuks

web/timeline: subscribe to event sender state

This commit is contained in:
Tulir Asokan 2024-10-21 22:53:19 +03:00
parent ab06dbf5aa
commit 3296c38454
7 changed files with 38 additions and 14 deletions

View file

@ -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 { 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

View file

@ -75,6 +75,7 @@ export class RoomStateStore {
readonly eventsByRowID: Map<EventRowID, MemDBEvent> = new Map()
readonly eventsByID: Map<EventID, MemDBEvent> = new Map()
readonly timelineSub = new Subscribable()
readonly stateSubs: Map<EventID, Subscribable> = new Map()
readonly eventSubs: Map<EventID, EventSubscribable> = new Map()
readonly openNotifications: Map<EventRowID, Notification> = 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) {

View file

@ -15,7 +15,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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 <blockquote
@ -84,7 +84,7 @@ export const ReplyBody = ({ room, event, onClose }: ReplyBodyProps) => {
{onClose && <button className="close-reply" onClick={onClose}><CloseButton/></button>}
</div>
<ContentErrorBoundary>
<BodyType room={room} event={event}/>
<BodyType room={room} event={event} sender={memberEvt}/>
</ContentErrorBoundary>
</blockquote>
}

View file

@ -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"
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<EventContentProps>): 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)

View file

@ -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 = <TextMessageBody event={event} room={room} />
caption = <TextMessageBody event={event} room={room} sender={sender} />
}
const [mediaContent, containerClass, containerStyle] = useMediaContent(content, event.type)
return <>

View file

@ -13,7 +13,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 { MemberEventContent, MessageEventContent } from "@/api/types"
import { MessageEventContent } from "@/api/types"
import EventContentProps from "./props.ts"
const onClickHTML = (evt: React.MouseEvent<HTMLDivElement>) => {
@ -23,7 +23,7 @@ const onClickHTML = (evt: React.MouseEvent<HTMLDivElement>) => {
}
}
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")

View file

@ -19,5 +19,5 @@ import { MemDBEvent } from "@/api/types"
export default interface EventContentProps {
room: RoomStateStore
event: MemDBEvent
sender?: MemDBEvent
sender: MemDBEvent | null
}