mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-19 18:13:41 -05:00
web: make receipts and typing update after fetching member events
This commit is contained in:
parent
0fbf76af98
commit
10d3da6e7a
9 changed files with 75 additions and 51 deletions
|
@ -37,7 +37,7 @@ export default class Client {
|
|||
readonly initComplete = new NonNullCachedEventDispatcher<boolean>(false)
|
||||
readonly store = new StateStore()
|
||||
#stateRequests: RoomStateGUID[] = []
|
||||
#stateRequestQueued = false
|
||||
#stateRequestPromise: Promise<void> | null = null
|
||||
#gcInterval: number | undefined
|
||||
|
||||
constructor(readonly rpc: RPCClient) {
|
||||
|
@ -126,21 +126,25 @@ export default class Client {
|
|||
room = this.store.rooms.get(room)
|
||||
}
|
||||
if (!room || room.state.get("m.room.member")?.has(userID) || room.requestedMembers.has(userID)) {
|
||||
return
|
||||
return null
|
||||
}
|
||||
room.requestedMembers.add(userID)
|
||||
this.#stateRequests.push({ room_id: room.roomID, type: "m.room.member", state_key: userID })
|
||||
if (!this.#stateRequestQueued) {
|
||||
this.#stateRequestQueued = true
|
||||
window.queueMicrotask(this.doStateRequests)
|
||||
if (this.#stateRequestPromise === null) {
|
||||
this.#stateRequestPromise = new Promise(this.#doStateRequestsPromise)
|
||||
}
|
||||
return this.#stateRequestPromise
|
||||
}
|
||||
|
||||
doStateRequests = () => {
|
||||
#doStateRequestsPromise = (resolve: () => void) => {
|
||||
window.queueMicrotask(() => {
|
||||
const reqs = this.#stateRequests
|
||||
this.#stateRequestQueued = false
|
||||
this.#stateRequestPromise = null
|
||||
this.#stateRequests = []
|
||||
this.loadSpecificRoomState(reqs).catch(err => console.error("Failed to load room state", reqs, err))
|
||||
this.loadSpecificRoomState(reqs)
|
||||
.catch(err => console.error("Failed to load room state", reqs, err))
|
||||
.finally(resolve)
|
||||
})
|
||||
}
|
||||
|
||||
requestEvent(room: RoomStateStore | RoomID | undefined, eventID: EventID) {
|
||||
|
|
|
@ -13,9 +13,18 @@
|
|||
//
|
||||
// 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 { useEffect, useMemo, useState, useSyncExternalStore } from "react"
|
||||
import { useEffect, useMemo, useReducer, useState, useSyncExternalStore } from "react"
|
||||
import Client from "@/api/client.ts"
|
||||
import type { CustomEmojiPack } from "@/util/emoji"
|
||||
import type { EventID, EventType, MemDBEvent, MemReceipt, UnknownEventContent } from "../types"
|
||||
import type {
|
||||
EventID,
|
||||
EventType,
|
||||
MemDBEvent,
|
||||
MemReceipt,
|
||||
MemberEventContent,
|
||||
UnknownEventContent,
|
||||
UserID,
|
||||
} from "../types"
|
||||
import { Preferences, preferences } from "../types/preferences"
|
||||
import type { StateStore } from "./main.ts"
|
||||
import type { AutocompleteMemberEntry, RoomStateStore } from "./room.ts"
|
||||
|
@ -48,6 +57,34 @@ export function useRoomState(
|
|||
)
|
||||
}
|
||||
|
||||
export function useRoomMember(
|
||||
client: Client | undefined | null, room: RoomStateStore | undefined, userID: UserID,
|
||||
): MemDBEvent | null {
|
||||
const evt = useRoomState(room, "m.room.member", userID)
|
||||
if (!evt && client && room) {
|
||||
client.requestMemberEvent(room, userID)
|
||||
}
|
||||
return evt
|
||||
}
|
||||
|
||||
export function useMultipleRoomMembers(
|
||||
client: Client, room: RoomStateStore, userIDs: UserID[],
|
||||
): [UserID, MemberEventContent | null][] {
|
||||
const [, forceUpdate] = useReducer(x => x + 1, 0)
|
||||
let promiseAwaited = false
|
||||
return userIDs.map(userID => {
|
||||
const evt = room.getStateEvent("m.room.member", userID)
|
||||
if (!evt) {
|
||||
const promise = client.requestMemberEvent(room, userID)
|
||||
if (promise && !promiseAwaited) {
|
||||
promiseAwaited = true
|
||||
promise.then(forceUpdate)
|
||||
}
|
||||
}
|
||||
const member = (evt?.content ?? null) as MemberEventContent | null
|
||||
return [userID, member]
|
||||
})
|
||||
}
|
||||
|
||||
export function useRoomMembers(room?: RoomStateStore): AutocompleteMemberEntry[] {
|
||||
return useSyncExternalStore(
|
||||
|
|
|
@ -281,11 +281,12 @@ export class RoomStateStore {
|
|||
}
|
||||
const oldArr = this.receiptsByEventID.get(existingReceipt.event_id)
|
||||
if (oldArr) {
|
||||
const idx = oldArr.indexOf(existingReceipt)
|
||||
if (idx >= 0) {
|
||||
oldArr.splice(idx, 1)
|
||||
if (oldArr.length === 0) {
|
||||
const updated = oldArr.filter(r => r !== existingReceipt)
|
||||
if (updated.length !== oldArr.length) {
|
||||
if (updated.length === 0) {
|
||||
this.receiptsByEventID.delete(existingReceipt.event_id)
|
||||
} else {
|
||||
this.receiptsByEventID.set(existingReceipt.event_id, updated)
|
||||
}
|
||||
this.receiptSubs.notify(existingReceipt.event_id)
|
||||
}
|
||||
|
|
|
@ -16,9 +16,9 @@
|
|||
import { JSX, use } from "react"
|
||||
import { PulseLoader } from "react-spinners"
|
||||
import { getAvatarURL } from "@/api/media.ts"
|
||||
import { useRoomTyping } from "@/api/statestore"
|
||||
import { MemberEventContent } from "@/api/types/mxtypes.ts"
|
||||
import { useMultipleRoomMembers, useRoomTyping } from "@/api/statestore"
|
||||
import { humanJoin } from "@/util/join.ts"
|
||||
import { getDisplayname } from "@/util/validation.ts"
|
||||
import ClientContext from "../ClientContext.ts"
|
||||
import { useRoomContext } from "../roomview/roomcontext.ts"
|
||||
import "./TypingNotifications.css"
|
||||
|
@ -28,19 +28,14 @@ const TypingNotifications = () => {
|
|||
const client = use(ClientContext)!
|
||||
const room = roomCtx.store
|
||||
const typing = useRoomTyping(room).filter(u => u !== client.userID)
|
||||
const memberEvts = useMultipleRoomMembers(client, room, typing.slice(0, 5))
|
||||
let loader: JSX.Element | null = null
|
||||
if (typing.length > 0) {
|
||||
loader = <PulseLoader speedMultiplier={0.5} size={5} color="var(--primary-color)" />
|
||||
}
|
||||
const avatars: JSX.Element[] = []
|
||||
const memberNames: string[] = []
|
||||
for (let i = 0; i < 5 && i < typing.length; i++) {
|
||||
const sender = typing[i]
|
||||
const memberEvt = room.getStateEvent("m.room.member", sender)
|
||||
const member = (memberEvt?.content ?? null) as MemberEventContent | null
|
||||
if (!memberEvt) {
|
||||
use(ClientContext)?.requestMemberEvent(room, sender)
|
||||
}
|
||||
for (const [sender, member] of memberEvts) {
|
||||
avatars.push(<img
|
||||
key={sender}
|
||||
className="small avatar"
|
||||
|
@ -48,7 +43,7 @@ const TypingNotifications = () => {
|
|||
src={getAvatarURL(sender, member)}
|
||||
alt=""
|
||||
/>)
|
||||
memberNames.push(member?.displayname ?? sender)
|
||||
memberNames.push(getDisplayname(sender, member))
|
||||
}
|
||||
|
||||
let description: JSX.Element | null = null
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
import { use, useEffect, useState } from "react"
|
||||
import { PuffLoader } from "react-spinners"
|
||||
import { getAvatarURL } from "@/api/media.ts"
|
||||
import { useRoomState } from "@/api/statestore"
|
||||
import { useRoomMember } from "@/api/statestore"
|
||||
import { MemberEventContent, UserID, UserProfile } from "@/api/types"
|
||||
import { getLocalpart } from "@/util/validation.ts"
|
||||
import ClientContext from "../ClientContext.ts"
|
||||
|
@ -34,11 +34,8 @@ const UserInfo = ({ userID }: UserInfoProps) => {
|
|||
const client = use(ClientContext)!
|
||||
const roomCtx = use(RoomContext)
|
||||
const openLightbox = use(LightboxContext)!
|
||||
const memberEvt = useRoomState(roomCtx?.store, "m.room.member", userID)
|
||||
const memberEvt = useRoomMember(client, roomCtx?.store, userID)
|
||||
const member = (memberEvt?.content ?? null) as MemberEventContent | null
|
||||
if (!memberEvt) {
|
||||
use(ClientContext)?.requestMemberEvent(roomCtx?.store, userID)
|
||||
}
|
||||
const [globalProfile, setGlobalProfile] = useState<UserProfile | null>(null)
|
||||
const [errors, setErrors] = useState<string[] | null>(null)
|
||||
useEffect(() => {
|
||||
|
|
|
@ -15,28 +15,22 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { use } from "react"
|
||||
import { getAvatarURL } from "@/api/media.ts"
|
||||
import { RoomStateStore, useReadReceipts } from "@/api/statestore"
|
||||
import { EventID, MemberEventContent } from "@/api/types"
|
||||
import { RoomStateStore, useMultipleRoomMembers, useReadReceipts } from "@/api/statestore"
|
||||
import { EventID } from "@/api/types"
|
||||
import { humanJoin } from "@/util/join.ts"
|
||||
import { getDisplayname } from "@/util/validation.ts"
|
||||
import ClientContext from "../ClientContext.ts"
|
||||
import "./ReadReceipts.css"
|
||||
|
||||
const ReadReceipts = ({ room, eventID }: { room: RoomStateStore, eventID: EventID }) => {
|
||||
const client = use(ClientContext)!
|
||||
const receipts = useReadReceipts(room, eventID)
|
||||
const memberEvts = useMultipleRoomMembers(client, room, receipts.map(receipt => receipt.user_id))
|
||||
if (receipts.length === 0) {
|
||||
return null
|
||||
}
|
||||
// Hacky hack for mobile clients. Would be nicer to get the number based on the CSS variable defining the size
|
||||
const maxAvatarCount = window.innerWidth > 720 ? 4 : 2
|
||||
const memberEvts = receipts.map(receipt => {
|
||||
const memberEvt = room.getStateEvent("m.room.member", receipt.user_id)
|
||||
const member = (memberEvt?.content ?? null) as MemberEventContent | null
|
||||
if (!memberEvt) {
|
||||
use(ClientContext)?.requestMemberEvent(room, receipt.user_id)
|
||||
}
|
||||
return [receipt.user_id, member] as const
|
||||
})
|
||||
const avatarMembers = receipts.length > maxAvatarCount ? memberEvts.slice(-maxAvatarCount+1) : memberEvts
|
||||
const avatars = avatarMembers.map(([userID, member]) => {
|
||||
return <img
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { use } from "react"
|
||||
import { getAvatarURL, getUserColorIndex } from "@/api/media.ts"
|
||||
import { RoomStateStore, useRoomEvent, useRoomState } from "@/api/statestore"
|
||||
import { RoomStateStore, useRoomEvent, useRoomMember } from "@/api/statestore"
|
||||
import type { EventID, MemDBEvent, MemberEventContent } from "@/api/types"
|
||||
import { getDisplayname } from "@/util/validation.ts"
|
||||
import ClientContext from "../ClientContext.ts"
|
||||
|
@ -80,10 +80,8 @@ const onClickReply = (evt: React.MouseEvent) => {
|
|||
export const ReplyBody = ({
|
||||
room, event, onClose, isThread, isEditing, isSilent, onSetSilent, isExplicitInThread, onSetExplicitInThread,
|
||||
}: ReplyBodyProps) => {
|
||||
const memberEvt = useRoomState(room, "m.room.member", event.sender)
|
||||
if (!memberEvt) {
|
||||
use(ClientContext)?.requestMemberEvent(room, event.sender)
|
||||
}
|
||||
const client = use(ClientContext)
|
||||
const memberEvt = useRoomMember(client, room, event.sender)
|
||||
const memberEvtContent = memberEvt?.content as MemberEventContent | undefined
|
||||
const BodyType = getBodyType(event, true)
|
||||
const classNames = ["reply-body"]
|
||||
|
|
|
@ -96,6 +96,7 @@ div.timeline-event {
|
|||
|
||||
> svg {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
&.error {
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import React, { use, useCallback, useState } from "react"
|
||||
import { getAvatarURL, getMediaURL, getUserColorIndex } from "@/api/media.ts"
|
||||
import { useRoomState } from "@/api/statestore"
|
||||
import { useRoomMember } from "@/api/statestore"
|
||||
import { MemDBEvent, MemberEventContent, UnreadType } from "@/api/types"
|
||||
import { isMobileDevice } from "@/util/ismobile.ts"
|
||||
import { getDisplayname, isEventID } from "@/util/validation.ts"
|
||||
|
@ -96,10 +96,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu }: TimelineEventProps) => {
|
|||
/>,
|
||||
})
|
||||
}, [openModal, evt, roomCtx])
|
||||
const memberEvt = useRoomState(roomCtx.store, "m.room.member", evt.sender)
|
||||
if (!memberEvt) {
|
||||
client.requestMemberEvent(roomCtx.store, evt.sender)
|
||||
}
|
||||
const memberEvt = useRoomMember(client, roomCtx.store, evt.sender)
|
||||
const memberEvtContent = memberEvt?.content as MemberEventContent | undefined
|
||||
const BodyType = getBodyType(evt)
|
||||
const eventTS = new Date(evt.timestamp)
|
||||
|
|
Loading…
Add table
Reference in a new issue