diff --git a/src/app/features/room/thread/ThreadsListPanel.tsx b/src/app/features/room/thread/ThreadsListPanel.tsx index c1e5c84f2..262ede7d4 100644 --- a/src/app/features/room/thread/ThreadsListPanel.tsx +++ b/src/app/features/room/thread/ThreadsListPanel.tsx @@ -14,6 +14,7 @@ import { Scroll, Text, config, + toRem, } from 'folds'; import classNames from 'classnames'; import { useVirtualizer } from '@tanstack/react-virtual'; @@ -45,6 +46,8 @@ import { import { useRoomThreads } from '../../../hooks/useRoomThreads'; import { useTimestampFormatter } from '../../../hooks/useTimestampFormatter'; import { formatRelativeAge } from '../../../utils/formatTimestamp'; +import { useRoomNavigate } from '../../../hooks/useRoomNavigate'; +import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; // Persisted across panel opens (the panel unmounts on close). getOnInit reads // localStorage synchronously so the chosen filter/sort apply on first render. @@ -118,8 +121,20 @@ type ThreadRowProps = { highlight: number; participants: string[]; onOpen: (threadId: string) => void; + onJump: (threadId: string) => void; + /** Phone layout: a bigger touch target for the jump button. */ + touch?: boolean; }; -function ThreadRow({ room, thread, unread, highlight, participants, onOpen }: ThreadRowProps) { +function ThreadRow({ + room, + thread, + unread, + highlight, + participants, + onOpen, + onJump, + touch, +}: ThreadRowProps) { const { prefs } = useTimestampFormatter(); const rootEvent = thread.rootEvent; const rootSender = rootEvent?.getSender() ?? ''; @@ -136,64 +151,80 @@ function ThreadRow({ room, thread, unread, highlight, participants, onOpen }: Th }`; return ( - onOpen(thread.id)} - aria-label={ariaLabel} - > - - - {nameInitials(rootName)}} - /> - - - {rootName} - - {unread > 0 && ( - - 0} count={unread} /> - - )} - - - + onOpen(thread.id)} + aria-label={ariaLabel} > - {snippet} - - - - - - {count} {count === 1 ? 'reply' : 'replies'} - {typeof lastTs === 'number' ? ` · ${formatRelativeAge(lastTs, prefs)}` : ''} - - - {participants.slice(0, MAX_PARTICIPANTS).map((userId) => ( - - ))} - {extra > 0 && ( - - +{extra} - + {/* Leaves room for the "Go to message" button laid over this corner. */} + + + {nameInitials(rootName)}} + /> + + + {rootName} + + {unread > 0 && ( + + 0} count={unread} /> + )} + + + {snippet} + + + + + + {count} {count === 1 ? 'reply' : 'replies'} + {typeof lastTs === 'number' ? ` · ${formatRelativeAge(lastTs, prefs)}` : ''} + + + {participants.slice(0, MAX_PARTICIPANTS).map((userId) => ( + + ))} + {extra > 0 && ( + + +{extra} + + )} + + + {/* [Gitea #165] Jump to the thread's first message in the room. A sibling + of the row button, not nested in it (no buttons inside buttons). */} + onJump(thread.id)} + style={{ position: 'absolute', top: toRem(6), right: toRem(6) }} + > + + ); } @@ -204,6 +235,16 @@ export type ThreadsListPanelProps = { onOpenThread: (threadId: string) => void; }; export function ThreadsListPanel({ room, onClose, onOpenThread }: ThreadsListPanelProps) { + const { navigateRoom } = useRoomNavigate(); + const screenSize = useScreenSizeContext(); + const handleJump = useCallback( + (threadId: string) => { + navigateRoom(room.roomId, threadId); + // On a phone the list covers the timeline; get out of the way. + if (screenSize === ScreenSize.Mobile) onClose(); + }, + [navigateRoom, room.roomId, screenSize, onClose], + ); const threads = useRoomThreads(room); const threadNotifications = useAtomValue(threadNotificationsAtom); const [storedFilter, setFilter] = useAtom(threadFilterAtom); @@ -403,6 +444,8 @@ export function ThreadsListPanel({ room, onClose, onOpenThread }: ThreadsListPan highlight={highlightById.get(snap.id) ?? 0} participants={participantsById.get(snap.id) ?? []} onOpen={onOpenThread} + onJump={handleJump} + touch={screenSize === ScreenSize.Mobile} /> );