import { useEffect, useState } from 'react'; import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types'; import { Room, RoomEvent, RoomEventHandlerMap } from 'matrix-js-sdk'; import { StateEvent } from '../../types/matrix/room'; import { useStateEvent } from './useStateEvent'; import { useMatrixClient } from './useMatrixClient'; import { createAccountDataListStore } from './createAccountDataListStore'; export const useRoomAvatar = (room: Room, dm?: boolean): string | undefined => { const avatarEvent = useStateEvent(room, StateEvent.RoomAvatar); if (dm) { return room.getAvatarFallbackMember()?.getMxcAvatarUrl(); } const content = avatarEvent?.getContent(); const avatarMxc = content && typeof content.url === 'string' ? content.url : undefined; return avatarMxc; }; export const useRoomName = (room: Room): string => { const [name, setName] = useState(room.name); useEffect(() => { setName(room.name); const handleRoomNameChange: RoomEventHandlerMap[RoomEvent.Name] = () => { setName(room.name); }; room.on(RoomEvent.Name, handleRoomNameChange); return () => { room.removeListener(RoomEvent.Name, handleRoomNameChange); }; }, [room]); return name; }; export const LOCAL_ROOM_NAMES_KEY = 'io.lotus.room_names'; export type LocalRoomNamesContent = { rooms: Record }; type LocalRoomNamesMap = Record; // Shared, concurrency-safe store. See createAccountDataListStore for why the // snapshot + write queue must be module-scoped: setAccountData does not update // the SDK's local cache (it only resolves once the /sync echo lands), so a // plain read-modify-write against getAccountData can lose a rename that is // still in flight when a second rename is issued (fixed: back-to-back renames // of different rooms no longer clobber each other). const roomNamesStore = createAccountDataListStore({ eventType: LOCAL_ROOM_NAMES_KEY, read: (content) => content && typeof content === 'object' && typeof content.rooms === 'object' ? content.rooms : {}, write: (rooms) => ({ rooms }), }); export function getLocalRoomNamesContent( mx: ReturnType, ): LocalRoomNamesContent { return { rooms: roomNamesStore.getLatest(mx) }; } /** * Set (or clear, when `name` is empty) the local display name for a room. * Routed through the shared write queue so back-to-back renames of different * rooms are always computed from the latest snapshot instead of a stale one. */ export function setLocalRoomName( mx: ReturnType, roomId: string, name: string, ): Promise { return roomNamesStore.enqueueWrite(mx, (current) => { if (!name) { const { [roomId]: _removed, ...rest } = current; return rest; } return { ...current, [roomId]: name }; }); } export const useLocalRoomName = (room: Room): string => { const mx = useMatrixClient(); const localNames = roomNamesStore.useValue(mx); const [name, setName] = useState(room.name); useEffect(() => { setName(room.name); const handleRoomNameChange: RoomEventHandlerMap[RoomEvent.Name] = () => { setName(room.name); }; room.on(RoomEvent.Name, handleRoomNameChange); return () => { room.removeListener(RoomEvent.Name, handleRoomNameChange); }; }, [room]); return localNames[room.roomId] ?? name; }; export const useHasLocalRoomName = (roomId: string): boolean => { const mx = useMatrixClient(); const localNames = roomNamesStore.useValue(mx); return !!localNames[roomId]; }; export type RoomTopicContent = { topic: string; formatted_body?: string; format?: string; }; export const useRoomTopic = (room: Room): RoomTopicContent | undefined => { const topicEvent = useStateEvent(room, StateEvent.RoomTopic); const content = topicEvent?.getContent(); if (!content || typeof content.topic !== 'string') return undefined; return { topic: content.topic, formatted_body: typeof content.formatted_body === 'string' ? content.formatted_body : undefined, format: typeof content.format === 'string' ? content.format : undefined, }; }; export const useRoomJoinRule = (room: Room): RoomJoinRulesEventContent | undefined => { const mEvent = useStateEvent(room, StateEvent.RoomJoinRules); const joinRuleContent = mEvent?.getContent(); return joinRuleContent; };