import { DndContext, closestCenter, useSensor, useSensors, PointerSensor, KeyboardSensor, } from '@dnd-kit/core' import { SortableContext, verticalListSortingStrategy, useSortable, } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { IconDots, IconStarFilled, IconStar, } from '@tabler/icons-react' import { useThreads } from '@/hooks/useThreads' import { useLeftPanel } from '@/hooks/useLeftPanel' import { cn } from '@/lib/utils' import { useSmallScreen } from '@/hooks/useMediaQuery' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { useTranslation } from '@/i18n/react-i18next-compat' import { memo, useMemo, useState } from 'react' import { useNavigate, useMatches } from '@tanstack/react-router' import { RenameThreadDialog, DeleteThreadDialog } from '@/containers/dialogs' import { route } from '@/constants/routes' const SortableItem = memo(({ thread }: { thread: Thread }) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: thread.id, disabled: true }) const isSmallScreen = useSmallScreen() const { setLeftPanel } = useLeftPanel() const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, } const { toggleFavorite, deleteThread, renameThread } = useThreads() const { t } = useTranslation() const [openDropdown, setOpenDropdown] = useState(false) const navigate = useNavigate() // Check if current route matches this thread's detail page const matches = useMatches() const isActive = matches.some( (match) => match.routeId === '/threads/$threadId' && 'threadId' in match.params && match.params.threadId === thread.id ) const handleClick = () => { if (!isDragging) { // Only close panel and navigate if the thread is not already active if (!isActive) { if (isSmallScreen) setLeftPanel(false) navigate({ to: route.threadsDetail, params: { threadId: thread.id } }) } } } const plainTitleForRename = useMemo(() => { // Basic HTML stripping for simple span tags. // If thread.title is undefined or null, treat as empty string before replace. return (thread.title || '').replace(/]*>|<\/span>/g, '') }, [thread.title]) return (
{ e.preventDefault() e.stopPropagation() setOpenDropdown(true) }} className={cn( 'mb-1 rounded hover:bg-left-panel-fg/10 flex items-center justify-between gap-2 px-1.5 group/thread-list transition-all', isDragging ? 'cursor-move' : 'cursor-pointer', isActive && 'bg-left-panel-fg/10' )} >
{thread.title || t('common:newThread')}
setOpenDropdown(open)} > { e.preventDefault() e.stopPropagation() }} /> {thread.isFavorite ? ( { e.stopPropagation() toggleFavorite(thread.id) }} > {t('common:unstar')} ) : ( { e.stopPropagation() toggleFavorite(thread.id) }} > {t('common:star')} )} setOpenDropdown(false)} /> setOpenDropdown(false)} />
) }) type ThreadListProps = { threads: Thread[] isFavoriteSection?: boolean } function ThreadList({ threads }: ThreadListProps) { const sortedThreads = useMemo(() => { return threads.sort((a, b) => { return (b.updated || 0) - (a.updated || 0) }) }, [threads]) const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { delay: 200, tolerance: 5, }, }), useSensor(KeyboardSensor) ) return ( t.id)} strategy={verticalListSortingStrategy} > {sortedThreads.map((thread, index) => ( ))} ) } export default memo(ThreadList)