From 154f234d0cdf8faa48c84e7bd52ea71847883a42 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 15 Sep 2025 13:16:43 +0530 Subject: [PATCH] thread menu - WIP --- src/app/features/room/RoomViewHeader.tsx | 38 ++ .../room/threads-menu/ThreadsMenu.css.ts | 18 + .../room/threads-menu/ThreadsMenu.tsx | 456 ++++++++++++++++++ src/app/hooks/useRoomThreads.ts | 9 + 4 files changed, 521 insertions(+) create mode 100644 src/app/features/room/threads-menu/ThreadsMenu.css.ts create mode 100644 src/app/features/room/threads-menu/ThreadsMenu.tsx create mode 100644 src/app/hooks/useRoomThreads.ts diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx index 352ae4b5..b333436a 100644 --- a/src/app/features/room/RoomViewHeader.tsx +++ b/src/app/features/room/RoomViewHeader.tsx @@ -379,6 +379,44 @@ export function RoomViewHeader() { )} + + + Threads + + } + > + {(triggerRef) => ( + + {pinnedEvents.length > 0 && ( + + + {pinnedEvents.length} + + + )} + + + )} + ; + onOpen: (roomId: string, eventId: string) => void; +}; +function Threads({ room, eventId, renderContent, onOpen }: ThreadsProps) { + const pinnedEvent = useRoomEvent(room, eventId); + const useAuthentication = useMediaAuthentication(); + const mx = useMatrixClient(); + const direct = useIsDirectRoom(); + const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor'); + + const powerLevels = usePowerLevelsContext(); + const { getPowerLevel } = usePowerLevelsAPI(powerLevels); + const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels); + const theme = useTheme(); + const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags); + + const handleOpenClick: MouseEventHandler = (evt) => { + evt.stopPropagation(); + const evtId = evt.currentTarget.getAttribute('data-event-id'); + if (!evtId) return; + onOpen(room.roomId, evtId); + }; + + const renderOptions = () => ( + + + Open + + + ); + + if (pinnedEvent === undefined) return ; + if (pinnedEvent === null) + return ( + + + Failed to load message! + + {renderOptions()} + + ); + + const sender = pinnedEvent.getSender()!; + const displayName = getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender; + const senderAvatarMxc = getMemberAvatarMxc(room, sender); + const getContent = (() => pinnedEvent.getContent()) as GetContentCallback; + + const senderPowerLevel = getPowerLevel(sender); + const powerLevelTag = getPowerLevelTag(senderPowerLevel); + const tagColor = powerLevelTag?.color ? accessibleTagColors?.get(powerLevelTag.color) : undefined; + const tagIconSrc = powerLevelTag?.icon + ? getTagIconSrc(mx, useAuthentication, powerLevelTag.icon) + : undefined; + + const usernameColor = legacyUsernameColor || direct ? colorMXID(sender) : tagColor; + + return ( + + + } + /> + + + } + > + + + + + + {displayName} + + + {tagIconSrc && } + + + {renderOptions()} + + {pinnedEvent.replyEventId && ( + + )} + {renderContent(pinnedEvent.getType(), false, pinnedEvent, displayName, getContent)} + + ); +} + +type ThreadsMenuProps = { + room: Room; + requestClose: () => void; +}; +export const ThreadsMenu = forwardRef( + ({ room, requestClose }, ref) => { + const mx = useMatrixClient(); + + const pinnedEvents = useRoomPinnedEvents(room); + const sortedPinnedEvent = useMemo(() => Array.from(pinnedEvents).reverse(), [pinnedEvents]); + const useAuthentication = useMediaAuthentication(); + const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad'); + const [urlPreview] = useSetting(settingsAtom, 'urlPreview'); + const { navigateRoom } = useRoomNavigate(); + const scrollRef = useRef(null); + + const virtualizer = useVirtualizer({ + count: sortedPinnedEvent.length, + getScrollElement: () => scrollRef.current, + estimateSize: () => 75, + overscan: 4, + }); + + const mentionClickHandler = useMentionClickHandler(room.roomId); + const spoilerClickHandler = useSpoilerClickHandler(); + + const linkifyOpts = useMemo( + () => ({ + ...LINKIFY_OPTS, + render: factoryRenderLinkifyWithMention((href) => + renderMatrixMention(mx, room.roomId, href, makeMentionCustomProps(mentionClickHandler)) + ), + }), + [mx, room, mentionClickHandler] + ); + const htmlReactParserOptions = useMemo( + () => + getReactCustomHtmlParser(mx, room.roomId, { + linkifyOpts, + useAuthentication, + handleSpoilerClick: spoilerClickHandler, + handleMentionClick: mentionClickHandler, + }), + [mx, room, linkifyOpts, mentionClickHandler, spoilerClickHandler, useAuthentication] + ); + + const renderMatrixEvent = useMatrixEventRenderer<[MatrixEvent, string, GetContentCallback]>( + { + [MessageEvent.RoomMessage]: (event, displayName, getContent) => { + if (event.isRedacted()) { + return ( + + ); + } + + return ( + + ); + }, + [MessageEvent.RoomMessageEncrypted]: (event, displayName) => { + const eventId = event.getId()!; + const evtTimeline = room.getTimelineForEvent(eventId); + + const mEvent = evtTimeline?.getEvents().find((e) => e.getId() === eventId); + + if (!mEvent || !evtTimeline) { + return ( + + + {event.getType()} + {' event'} + + + ); + } + + return ( + + {() => { + if (mEvent.isRedacted()) return ; + if (mEvent.getType() === MessageEvent.Sticker) + return ( + ( + } + renderViewer={(p) => } + /> + )} + /> + ); + if (mEvent.getType() === MessageEvent.RoomMessage) { + const editedEvent = getEditedEvent(eventId, mEvent, evtTimeline.getTimelineSet()); + const getContent = (() => + editedEvent?.getContent()['m.new_content'] ?? + mEvent.getContent()) as GetContentCallback; + + return ( + + ); + } + if (mEvent.getType() === MessageEvent.RoomMessageEncrypted) + return ( + + + + ); + return ( + + + + ); + }} + + ); + }, + [MessageEvent.Sticker]: (event, displayName, getContent) => { + if (event.isRedacted()) { + return ( + + ); + } + return ( + ( + } + renderViewer={(p) => } + /> + )} + /> + ); + }, + }, + undefined, + (event) => { + if (event.isRedacted()) { + return ; + } + return ( + + + {event.getType()} + {' event'} + + + ); + } + ); + + const handleOpen = (roomId: string, eventId: string) => { + navigateRoom(roomId, eventId); + requestClose(); + }; + + return ( + + +
+ + Threads + + + + + + +
+ + + + {sortedPinnedEvent.length > 0 ? ( +
+ {virtualizer.getVirtualItems().map((vItem) => { + const eventId = sortedPinnedEvent[vItem.index]; + if (!eventId) return null; + + return ( + + + + + + ); + })} +
+ ) : ( + + + + + No Threads + + + This room does not have any threads yet. + + + + )} +
+
+
+
+
+ ); + } +); diff --git a/src/app/hooks/useRoomThreads.ts b/src/app/hooks/useRoomThreads.ts new file mode 100644 index 00000000..8050a5d9 --- /dev/null +++ b/src/app/hooks/useRoomThreads.ts @@ -0,0 +1,9 @@ +// import { Room } from 'matrix-js-sdk'; +// import { useMatrixClient } from './useMatrixClient'; + +// export const useRoomThreads = (room: Room) => { +// const mx = useMatrixClient(); + +// mx.createThreadListMessagesRequest; +// mx.processThreadRoots; +// };