Manila folders, ruled chat, post-it nav, and folder tabs; dark variant uses Catppuccin Mocha colors with muted stickies and soft glows.
182 lines
6.1 KiB
TypeScript
182 lines
6.1 KiB
TypeScript
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 (
|
|
<NavItem variant="Background" radii="400" aria-selected={selected}>
|
|
<NavButton onClick={handleClick}>
|
|
<NavItemContent>
|
|
<Box as="span" grow="Yes" alignItems="Center" gap="200">
|
|
<Avatar size="200" radii="400">
|
|
<Icon src={Icons.Thread} size="100" />
|
|
</Avatar>
|
|
<Box as="span" grow="Yes" direction="Column" style={{ overflow: 'hidden', minWidth: 0 }}>
|
|
<Text as="span" size="Inherit" truncate>
|
|
{senderName}
|
|
</Text>
|
|
{rootBody && (
|
|
<Text as="span" size="T200" priority="300" truncate>
|
|
{rootBody}
|
|
</Text>
|
|
)}
|
|
</Box>
|
|
{replyCount > 0 && (
|
|
<Badge variant="Secondary" size="400" fill="Soft" radii="Pill">
|
|
<Text as="span" size="L400">
|
|
{replyCount}
|
|
</Text>
|
|
</Badge>
|
|
)}
|
|
</Box>
|
|
</NavItemContent>
|
|
</NavButton>
|
|
</NavItem>
|
|
);
|
|
}
|
|
|
|
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<ThreadEntry[]>((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<ThreadEntry[]>(getParticipatedThreads);
|
|
|
|
// Re-compute when thread events fire in any home room.
|
|
useEffect(() => {
|
|
const handleUpdate = () => setThreads(getParticipatedThreads());
|
|
|
|
const roomObjects = rooms.reduce<Room[]>((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 (
|
|
<NavCategory data-nav-dropdown="">
|
|
<NavCategoryHeader>
|
|
<RoomNavCategoryButton
|
|
closed={closedCategories.has(THREADS_CATEGORY_ID)}
|
|
data-category-id={THREADS_CATEGORY_ID}
|
|
onClick={handleCategoryClick}
|
|
>
|
|
My Threads
|
|
</RoomNavCategoryButton>
|
|
</NavCategoryHeader>
|
|
{!closedCategories.has(THREADS_CATEGORY_ID) && (
|
|
<div data-nav-rooms="">
|
|
{threads.map(({ room, thread }) => (
|
|
<ThreadNavItem
|
|
key={`${room.roomId}:${thread.id}`}
|
|
room={room}
|
|
thread={thread}
|
|
selected={false}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</NavCategory>
|
|
);
|
|
}
|