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, IconTrash, IconEdit, IconStar, } from '@tabler/icons-react' import { useThreads } from '@/hooks/useThreads' import { useLeftPanel } from '@/hooks/useLeftPanel' import { cn } from '@/lib/utils' import { route } from '@/constants/routes' import { useSmallScreen } from '@/hooks/useMediaQuery' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { useTranslation } from '@/i18n/react-i18next-compat' import { DialogClose, DialogFooter, DialogHeader } from '@/components/ui/dialog' import { Dialog, DialogTrigger, DialogContent, DialogTitle, DialogDescription, } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { memo, useMemo, useState } from 'react' import { useNavigate, useMatches } from '@tanstack/react-router' import { toast } from 'sonner' import { Input } from '@/components/ui/input' 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]) const [title, setTitle] = useState( plainTitleForRename || t('common:newThread') ) return (
{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')} )} { if (!open) { setOpenDropdown(false) setTitle(plainTitleForRename || t('common:newThread')) } }} > e.preventDefault()}> {t('common:rename')} {t('common:threadTitle')} { setTitle(e.target.value) }} className="mt-2" onKeyDown={(e) => { // Prevent key from being captured by parent components e.stopPropagation() }} /> { if (!open) setOpenDropdown(false) }} > e.preventDefault()}> {t('common:delete')} {t('common:deleteThread')} {t('common:dialogs.deleteThread.description')}
) }) 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)