import React, { useCallback, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Avatar, Box, Text, Badge } from 'folds'; import { Icon, Icons } from '../../../components/icons'; import { useAtom, useSetAtom } from 'jotai'; import { Room, Thread, ThreadEvent } from 'matrix-js-sdk'; import { NavCategory, NavCategoryHeader, NavItem, NavItemContent, NavButton } from '../../../components/nav'; import { RoomNavCategoryButton } from '../../../features/room-nav'; import { makeNavCategoryId } from '../../../state/closedNavCategories'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useCategoryHandler } from '../../../hooks/useCategoryHandler'; import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories'; import { activeThreadIdAtomFamily } from '../../../state/activeThread'; import { getCanonicalAliasOrRoomId, getMxIdLocalPart } from '../../../utils/matrix'; import { getHomeRoomPath } from '../../pathUtils'; import { getMemberDisplayName } from '../../../utils/room'; const THREADS_CATEGORY_ID = makeNavCategoryId('home', 'threads'); type ThreadEntry = { room: Room; thread: Thread; }; type ThreadNavItemProps = { room: Room; thread: Thread; selected: boolean; }; /** * A single nav item representing a thread the user has participated in. * Navigates to the room and opens the thread when clicked. */ function ThreadNavItem({ room, thread, selected }: ThreadNavItemProps) { const navigate = useNavigate(); const setActiveThreadId = useSetAtom(activeThreadIdAtomFamily(room.roomId)); const mx = useMatrixClient(); const { rootEvent } = thread; const senderId = rootEvent?.getSender() ?? ''; const senderName = getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId; const rootBody = rootEvent?.getContent()?.body ?? ''; const replyCount = thread.length; const handleClick = useCallback(() => { const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId); navigate(getHomeRoomPath(roomIdOrAlias)); // Defer slightly so RoomView has time to render before we set the atom. requestAnimationFrame(() => { // eslint-disable-next-line no-console console.log('[HomeThreadsCategory] Opening thread', { threadId: thread.id, rootEventId: thread.rootEvent?.getId(), roomId: room.roomId, }); // Use rootEvent ID if available, fallback to thread.id const threadIdToUse = thread.rootEvent?.getId() || thread.id; setActiveThreadId(threadIdToUse); }); }, [navigate, mx, room.roomId, setActiveThreadId, thread.id, thread.rootEvent]); return ( {senderName} {rootBody && ( {rootBody} )} {replyCount > 0 && ( {replyCount} )} ); } type HomeThreadsCategoryProps = { rooms: string[]; }; /** * Nav category for the left sidebar showing threads in Home rooms where the * current user has participated. */ export function HomeThreadsCategory({ rooms }: HomeThreadsCategoryProps) { const mx = useMatrixClient(); const [closedCategories, setClosedCategories] = useAtom(useClosedNavCategoriesAtom()); const getParticipatedThreads = useCallback((): ThreadEntry[] => rooms.reduce((acc, roomId) => { const room = mx.getRoom(roomId); if (!room) return acc; return acc.concat( room .getThreads() .filter((t) => t.hasCurrentUserParticipated) .map((t) => ({ room, thread: t })) ); }, []), [mx, rooms]); const [threads, setThreads] = useState(getParticipatedThreads); // Re-compute when thread events fire in any home room. useEffect(() => { const handleUpdate = () => setThreads(getParticipatedThreads()); const roomObjects = rooms.reduce((acc, roomId) => { const room = mx.getRoom(roomId); if (room) acc.push(room); return acc; }, []); roomObjects.forEach((room) => { room.getThreads().forEach((thread) => { thread.on(ThreadEvent.Update, handleUpdate); thread.on(ThreadEvent.NewReply, handleUpdate); }); }); return () => { roomObjects.forEach((room) => { room.getThreads().forEach((thread) => { thread.off(ThreadEvent.Update, handleUpdate); thread.off(ThreadEvent.NewReply, handleUpdate); }); }); }; }, [mx, rooms, getParticipatedThreads]); const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) => closedCategories.has(categoryId) ); if (threads.length === 0) return null; return ( My Threads {!closedCategories.has(THREADS_CATEGORY_ID) && (
{threads.map(({ room, thread }) => ( ))}
)}
); }