forked from Mirrors/gomuks
web/timeline: subscribe to event sender state
This commit is contained in:
parent
ab06dbf5aa
commit
3296c38454
7 changed files with 38 additions and 14 deletions
|
@ -14,7 +14,7 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import { useSyncExternalStore } from "react"
|
import { useSyncExternalStore } from "react"
|
||||||
import type { EventID, MemDBEvent } from "../types"
|
import type { EventID, EventType, MemDBEvent } from "../types"
|
||||||
import { RoomStateStore } from "./room.ts"
|
import { RoomStateStore } from "./room.ts"
|
||||||
|
|
||||||
export function useRoomTimeline(room: RoomStateStore): (MemDBEvent | null)[] {
|
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 noopSubscribe = () => () => {}
|
||||||
const returnNull = () => null
|
const returnNull = () => null
|
||||||
|
|
||||||
|
|
|
@ -75,6 +75,7 @@ export class RoomStateStore {
|
||||||
readonly eventsByRowID: Map<EventRowID, MemDBEvent> = new Map()
|
readonly eventsByRowID: Map<EventRowID, MemDBEvent> = new Map()
|
||||||
readonly eventsByID: Map<EventID, MemDBEvent> = new Map()
|
readonly eventsByID: Map<EventID, MemDBEvent> = new Map()
|
||||||
readonly timelineSub = new Subscribable()
|
readonly timelineSub = new Subscribable()
|
||||||
|
readonly stateSubs: Map<EventID, Subscribable> = new Map()
|
||||||
readonly eventSubs: Map<EventID, EventSubscribable> = new Map()
|
readonly eventSubs: Map<EventID, EventSubscribable> = new Map()
|
||||||
readonly openNotifications: Map<EventRowID, Notification> = new Map()
|
readonly openNotifications: Map<EventRowID, Notification> = new Map()
|
||||||
readonly pendingEvents: EventRowID[] = []
|
readonly pendingEvents: EventRowID[] = []
|
||||||
|
@ -110,6 +111,21 @@ export class RoomStateStore {
|
||||||
return sub
|
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 {
|
getStateEvent(type: EventType, stateKey: string): MemDBEvent | undefined {
|
||||||
const rowID = this.state.get(type)?.get(stateKey)
|
const rowID = this.state.get(type)?.get(stateKey)
|
||||||
if (!rowID) {
|
if (!rowID) {
|
||||||
|
@ -200,6 +216,7 @@ export class RoomStateStore {
|
||||||
}
|
}
|
||||||
for (const [key, rowID] of Object.entries(changedEvts)) {
|
for (const [key, rowID] of Object.entries(changedEvts)) {
|
||||||
stateMap.set(key, rowID)
|
stateMap.set(key, rowID)
|
||||||
|
this.notifyStateSubscribers(evtType, key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sync.reset) {
|
if (sync.reset) {
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import { use } from "react"
|
import { use } from "react"
|
||||||
import { getAvatarURL } from "@/api/media.ts"
|
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 type { EventID, MemDBEvent, MemberEventContent } from "@/api/types"
|
||||||
import { ClientContext } from "../ClientContext.ts"
|
import { ClientContext } from "../ClientContext.ts"
|
||||||
import getBodyType, { ContentErrorBoundary } from "./content"
|
import getBodyType, { ContentErrorBoundary } from "./content"
|
||||||
|
@ -63,7 +63,7 @@ const onClickReply = (evt: React.MouseEvent) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ReplyBody = ({ room, event, onClose }: ReplyBodyProps) => {
|
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 memberEvtContent = memberEvt?.content as MemberEventContent | undefined
|
||||||
const BodyType = getBodyType(event, true)
|
const BodyType = getBodyType(event, true)
|
||||||
return <blockquote
|
return <blockquote
|
||||||
|
@ -84,7 +84,7 @@ export const ReplyBody = ({ room, event, onClose }: ReplyBodyProps) => {
|
||||||
{onClose && <button className="close-reply" onClick={onClose}><CloseButton/></button>}
|
{onClose && <button className="close-reply" onClick={onClose}><CloseButton/></button>}
|
||||||
</div>
|
</div>
|
||||||
<ContentErrorBoundary>
|
<ContentErrorBoundary>
|
||||||
<BodyType room={room} event={event}/>
|
<BodyType room={room} event={event} sender={memberEvt}/>
|
||||||
</ContentErrorBoundary>
|
</ContentErrorBoundary>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import React, { use, useCallback } from "react"
|
import React, { use, useCallback } from "react"
|
||||||
import { getAvatarURL } from "@/api/media.ts"
|
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 { EventID, MemDBEvent, MemberEventContent, UnreadType } from "@/api/types"
|
||||||
import { isEventID } from "@/util/validation.ts"
|
import { isEventID } from "@/util/validation.ts"
|
||||||
import { ClientContext } from "../ClientContext.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 TimelineEvent = ({ room, evt, prevEvt, setReplyToRef }: TimelineEventProps) => {
|
||||||
const wrappedSetReplyTo = useCallback(() => setReplyToRef.current(evt.event_id), [evt, setReplyToRef])
|
const wrappedSetReplyTo = useCallback(() => setReplyToRef.current(evt.event_id), [evt, setReplyToRef])
|
||||||
const client = use(ClientContext)!
|
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 memberEvtContent = memberEvt?.content as MemberEventContent | undefined
|
||||||
const BodyType = getBodyType(evt)
|
const BodyType = getBodyType(evt)
|
||||||
const eventTS = new Date(evt.timestamp)
|
const eventTS = new Date(evt.timestamp)
|
||||||
|
|
|
@ -18,11 +18,11 @@ import TextMessageBody from "./TextMessageBody.tsx"
|
||||||
import EventContentProps from "./props.ts"
|
import EventContentProps from "./props.ts"
|
||||||
import { useMediaContent } from "./useMediaContent.tsx"
|
import { useMediaContent } from "./useMediaContent.tsx"
|
||||||
|
|
||||||
const MediaMessageBody = ({ event, room }: EventContentProps) => {
|
const MediaMessageBody = ({ event, room, sender }: EventContentProps) => {
|
||||||
const content = event.content as MediaMessageEventContent
|
const content = event.content as MediaMessageEventContent
|
||||||
let caption = null
|
let caption = null
|
||||||
if (content.body && content.filename && content.body !== content.filename) {
|
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)
|
const [mediaContent, containerClass, containerStyle] = useMediaContent(content, event.type)
|
||||||
return <>
|
return <>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// 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"
|
import EventContentProps from "./props.ts"
|
||||||
|
|
||||||
const onClickHTML = (evt: React.MouseEvent<HTMLDivElement>) => {
|
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 content = event.content as MessageEventContent
|
||||||
const classNames = ["message-text"]
|
const classNames = ["message-text"]
|
||||||
let eventSenderName: string | undefined
|
let eventSenderName: string | undefined
|
||||||
|
@ -31,9 +31,7 @@ const TextMessageBody = ({ event, room }: EventContentProps) => {
|
||||||
classNames.push("notice-message")
|
classNames.push("notice-message")
|
||||||
} else if (content.msgtype === "m.emote") {
|
} else if (content.msgtype === "m.emote") {
|
||||||
classNames.push("emote-message")
|
classNames.push("emote-message")
|
||||||
const memberEvt = room.getStateEvent("m.room.member", event.sender)
|
eventSenderName = sender?.content?.displayname || event.sender
|
||||||
const memberEvtContent = memberEvt?.content as MemberEventContent | undefined
|
|
||||||
eventSenderName = memberEvtContent?.displayname || event.sender
|
|
||||||
}
|
}
|
||||||
if (event.local_content?.sanitized_html) {
|
if (event.local_content?.sanitized_html) {
|
||||||
classNames.push("html-body")
|
classNames.push("html-body")
|
||||||
|
|
|
@ -19,5 +19,5 @@ import { MemDBEvent } from "@/api/types"
|
||||||
export default interface EventContentProps {
|
export default interface EventContentProps {
|
||||||
room: RoomStateStore
|
room: RoomStateStore
|
||||||
event: MemDBEvent
|
event: MemDBEvent
|
||||||
sender?: MemDBEvent
|
sender: MemDBEvent | null
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue