feat: personal room name overrides (MSC4431-style)
Users can right-click any room and 'Rename for me...' to set a local display name visible only to them. Stored in account data under io.lotus.room_names. Shows a pencil indicator on renamed rooms. useLocalRoomName() hook overrides useRoomName() when a local name exists. Also includes: - Rich room topic rendering via RoomTopicContent object (formatted_body support in RoomTopicViewer with HTML sanitization via sanitizeCustomHtml) - Edit history viewer: clicking '(edited)' on a message opens a modal showing all prior versions with timestamps (EditHistoryModal.tsx) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
|
||||
import { Room, RoomEvent, RoomEventHandlerMap } from 'matrix-js-sdk';
|
||||
import { ClientEvent, MatrixEvent, Room, RoomEvent, RoomEventHandlerMap } from 'matrix-js-sdk';
|
||||
import { StateEvent } from '../../types/matrix/room';
|
||||
import { useStateEvent } from './useStateEvent';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
|
||||
export const useRoomAvatar = (room: Room, dm?: boolean): string | undefined => {
|
||||
const avatarEvent = useStateEvent(room, StateEvent.RoomAvatar);
|
||||
@@ -34,13 +35,102 @@ export const useRoomName = (room: Room): string => {
|
||||
return name;
|
||||
};
|
||||
|
||||
export const useRoomTopic = (room: Room): string | undefined => {
|
||||
export const LOCAL_ROOM_NAMES_KEY = 'io.lotus.room_names';
|
||||
|
||||
export type LocalRoomNamesContent = { rooms: Record<string, string> };
|
||||
|
||||
function getLocalRoomNamesContent(mx: ReturnType<typeof useMatrixClient>): LocalRoomNamesContent {
|
||||
// Use any-cast because LOCAL_ROOM_NAMES_KEY is not in matrix-js-sdk AccountDataEvents
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const raw: unknown = (mx as any).getAccountData(LOCAL_ROOM_NAMES_KEY)?.getContent();
|
||||
if (
|
||||
raw &&
|
||||
typeof raw === 'object' &&
|
||||
'rooms' in raw &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
typeof (raw as any).rooms === 'object'
|
||||
) {
|
||||
return raw as LocalRoomNamesContent;
|
||||
}
|
||||
return { rooms: {} };
|
||||
}
|
||||
|
||||
export const useLocalRoomName = (room: Room): string => {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const getLocalName = useCallback((): string => {
|
||||
const content = getLocalRoomNamesContent(mx);
|
||||
return content.rooms[room.roomId] ?? room.name;
|
||||
}, [mx, room]);
|
||||
|
||||
const [name, setName] = useState(getLocalName);
|
||||
|
||||
useEffect(() => {
|
||||
setName(getLocalName());
|
||||
|
||||
const handleAccountData = (event: MatrixEvent) => {
|
||||
if (event.getType() !== LOCAL_ROOM_NAMES_KEY) return;
|
||||
setName(getLocalName());
|
||||
};
|
||||
mx.on(ClientEvent.AccountData, handleAccountData);
|
||||
|
||||
const handleRoomNameChange: RoomEventHandlerMap[RoomEvent.Name] = () => {
|
||||
setName(getLocalName());
|
||||
};
|
||||
room.on(RoomEvent.Name, handleRoomNameChange);
|
||||
|
||||
return () => {
|
||||
mx.removeListener(ClientEvent.AccountData, handleAccountData);
|
||||
room.removeListener(RoomEvent.Name, handleRoomNameChange);
|
||||
};
|
||||
}, [mx, room, getLocalName]);
|
||||
|
||||
return name;
|
||||
};
|
||||
|
||||
export const useHasLocalRoomName = (roomId: string): boolean => {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const check = useCallback((): boolean => {
|
||||
const content = getLocalRoomNamesContent(mx);
|
||||
return !!content.rooms[roomId];
|
||||
}, [mx, roomId]);
|
||||
|
||||
const [hasLocal, setHasLocal] = useState(check);
|
||||
|
||||
useEffect(() => {
|
||||
setHasLocal(check());
|
||||
|
||||
const handleAccountData = (event: MatrixEvent) => {
|
||||
if (event.getType() !== LOCAL_ROOM_NAMES_KEY) return;
|
||||
setHasLocal(check());
|
||||
};
|
||||
mx.on(ClientEvent.AccountData, handleAccountData);
|
||||
return () => {
|
||||
mx.removeListener(ClientEvent.AccountData, handleAccountData);
|
||||
};
|
||||
}, [mx, check]);
|
||||
|
||||
return hasLocal;
|
||||
};
|
||||
|
||||
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();
|
||||
const topic = content && typeof content.topic === 'string' ? content.topic : undefined;
|
||||
if (!content || typeof content.topic !== 'string') return undefined;
|
||||
|
||||
return topic;
|
||||
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 => {
|
||||
|
||||
Reference in New Issue
Block a user