feat(threads): room-level Threads list panel
Lotus could view one thread at a time but had no overview of a room's threads. Add a Threads list side panel, opened from a new Threads toggle in the room header (mirrors the gallery/widgets toggles). - Lists every thread with a rich row: root sender + snippet, unread dot, "N replies - last reply <time>", and a participant avatar pile. - Segmented filter (All / Unread / Participating) and sort (Recent / Oldest by last-reply time), both persisted in localStorage (cinny_threads_filter_v1 / cinny_threads_sort_v1) and normalized via type guards. - Clicking a row opens the existing single-thread ThreadPanel by reusing setActiveThreadId; reading it clears the row's unread badge live. - Stays live via ThreadEvent.New/NewReply/Update/Delete + RoomEvent.UnreadNotifications, with a signature guard to avoid churn, and is virtualized (@tanstack/react-virtual) for busy rooms. Reuses room.getThreads()/fetchRoomThreads(), thread.hasCurrentUser- Participated / lastReply() / length, getThreadUnreadNotificationCount (muted threads zeroed), useMemberAvatar/StackedAvatar/UserAvatar, scaleSystemEmoji/trimReplyFromBody, UnreadBadge, and the Bookmarks-panel segmented-control + localStorage-atom patterns. Filter/sort logic is pure in utils/threadList.ts with 8 unit tests. New panel is wired into Room.tsx's mutually-exclusive content-panel switching. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,9 @@ import { CallChatView } from './CallChatView';
|
||||
import { useCallEmbed } from '../../hooks/useCallEmbed';
|
||||
import { useCallMembers, useCallSession } from '../../hooks/useCall';
|
||||
import { roomIdToActiveThreadIdAtomFamily } from '../../state/room/thread';
|
||||
import { threadsListAtom } from '../../state/threadsList';
|
||||
import { ThreadPanel } from './thread';
|
||||
import { ThreadsListPanel } from './thread/ThreadsListPanel';
|
||||
|
||||
export function Room() {
|
||||
const { eventId } = useParams();
|
||||
@@ -43,6 +45,8 @@ export function Room() {
|
||||
const setGalleryOpen = useSetAtom(mediaGalleryAtom);
|
||||
const widgetsOpen = useAtomValue(widgetsPanelAtom);
|
||||
const setWidgetsOpen = useSetAtom(widgetsPanelAtom);
|
||||
const threadsListOpen = useAtomValue(threadsListAtom);
|
||||
const setThreadsListOpen = useSetAtom(threadsListAtom);
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const screenSize = useScreenSizeContext();
|
||||
const powerLevels = usePowerLevels(room);
|
||||
@@ -74,24 +78,43 @@ export function Room() {
|
||||
const prevThreadRef = useRef(activeThreadId);
|
||||
const prevGalleryRef = useRef(galleryOpen);
|
||||
const prevWidgetsRef = useRef(widgetsOpen);
|
||||
const prevThreadsListRef = useRef(threadsListOpen);
|
||||
useEffect(() => {
|
||||
const threadJustOpened = Boolean(activeThreadId) && !prevThreadRef.current;
|
||||
const galleryJustOpened = galleryOpen && !prevGalleryRef.current;
|
||||
const widgetsJustOpened = widgetsOpen && !prevWidgetsRef.current;
|
||||
const threadsListJustOpened = threadsListOpen && !prevThreadsListRef.current;
|
||||
if (threadJustOpened) {
|
||||
if (galleryOpen) setGalleryOpen(false);
|
||||
if (widgetsOpen) setWidgetsOpen(false);
|
||||
if (threadsListOpen) setThreadsListOpen(false);
|
||||
} else if (galleryJustOpened) {
|
||||
if (activeThreadId) setActiveThreadId(null);
|
||||
if (widgetsOpen) setWidgetsOpen(false);
|
||||
if (threadsListOpen) setThreadsListOpen(false);
|
||||
} else if (widgetsJustOpened) {
|
||||
if (activeThreadId) setActiveThreadId(null);
|
||||
if (galleryOpen) setGalleryOpen(false);
|
||||
if (threadsListOpen) setThreadsListOpen(false);
|
||||
} else if (threadsListJustOpened) {
|
||||
if (activeThreadId) setActiveThreadId(null);
|
||||
if (galleryOpen) setGalleryOpen(false);
|
||||
if (widgetsOpen) setWidgetsOpen(false);
|
||||
}
|
||||
prevThreadRef.current = activeThreadId;
|
||||
prevGalleryRef.current = galleryOpen;
|
||||
prevWidgetsRef.current = widgetsOpen;
|
||||
}, [activeThreadId, galleryOpen, widgetsOpen, setGalleryOpen, setActiveThreadId, setWidgetsOpen]);
|
||||
prevThreadsListRef.current = threadsListOpen;
|
||||
}, [
|
||||
activeThreadId,
|
||||
galleryOpen,
|
||||
widgetsOpen,
|
||||
threadsListOpen,
|
||||
setGalleryOpen,
|
||||
setActiveThreadId,
|
||||
setWidgetsOpen,
|
||||
setThreadsListOpen,
|
||||
]);
|
||||
|
||||
// On non-desktop screens at most one right-side panel may show, priority
|
||||
// thread > gallery > widgets > members. On desktop thread + members may coexist
|
||||
@@ -100,8 +123,16 @@ export function Room() {
|
||||
const showThreadPanel = !callView && Boolean(activeThreadId);
|
||||
const showGallery = !callView && galleryOpen && (isDesktop || !activeThreadId);
|
||||
const showWidgets = !callView && widgetsOpen && (isDesktop || (!activeThreadId && !galleryOpen));
|
||||
// The single-thread panel always replaces the list (they share the content slot).
|
||||
const showThreadsList =
|
||||
!callView &&
|
||||
threadsListOpen &&
|
||||
!activeThreadId &&
|
||||
(isDesktop || (!galleryOpen && !widgetsOpen));
|
||||
const showMembers =
|
||||
!callView && isDrawer && (isDesktop || (!activeThreadId && !galleryOpen && !widgetsOpen));
|
||||
!callView &&
|
||||
isDrawer &&
|
||||
(isDesktop || (!activeThreadId && !galleryOpen && !widgetsOpen && !threadsListOpen));
|
||||
|
||||
return (
|
||||
<PowerLevelsContextProvider value={powerLevels}>
|
||||
@@ -151,6 +182,22 @@ export function Room() {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{showThreadsList && (
|
||||
<>
|
||||
{screenSize === ScreenSize.Desktop && (
|
||||
<Line variant="Background" direction="Vertical" size="300" />
|
||||
)}
|
||||
<ThreadsListPanel
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
onClose={() => setThreadsListOpen(false)}
|
||||
onOpenThread={(threadId) => {
|
||||
setActiveThreadId(threadId);
|
||||
setThreadsListOpen(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{showThreadPanel && activeThreadId && (
|
||||
<>
|
||||
{screenSize === ScreenSize.Desktop && (
|
||||
|
||||
Reference in New Issue
Block a user