refactor(AllChatsSidebar): replace timestamp formatting with relative date display and adjust button styles

This commit is contained in:
Anish Sarkar 2026-07-15 21:12:51 +05:30
parent 7889babb8e
commit a466fdcf5d
2 changed files with 56 additions and 68 deletions

View file

@ -37,14 +37,13 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useActivateChatThread } from "@/hooks/use-activate-chat-thread"; import { useActivateChatThread } from "@/hooks/use-activate-chat-thread";
import { useDebouncedValue } from "@/hooks/use-debounced-value"; import { useDebouncedValue } from "@/hooks/use-debounced-value";
import { useLongPress } from "@/hooks/use-long-press"; import { useLongPress } from "@/hooks/use-long-press";
import { useIsMobile } from "@/hooks/use-mobile"; import { useIsMobile } from "@/hooks/use-mobile";
import { useArchiveThread, useDeleteThread, useRenameThread } from "@/hooks/use-thread-mutations"; import { useArchiveThread, useDeleteThread, useRenameThread } from "@/hooks/use-thread-mutations";
import { fetchThreads, searchThreads, type ThreadListItem } from "@/lib/chat/thread-persistence"; import { fetchThreads, searchThreads, type ThreadListItem } from "@/lib/chat/thread-persistence";
import { formatThreadTimestamp } from "@/lib/format-date"; import { formatRelativeDate } from "@/lib/format-date";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
interface AllChatsContentProps { interface AllChatsContentProps {
@ -288,7 +287,7 @@ function AllChatsContent({ workspaceId, className }: AllChatsContentProps) {
placeholder={t("search_chats") || "Search chats..."} placeholder={t("search_chats") || "Search chats..."}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="h-12 border-0 bg-muted pl-10 pr-9 text-base shadow-none" className="h-10 border-0 bg-muted pl-10 pr-9 text-base shadow-none"
/> />
{searchQuery && ( {searchQuery && (
<Button <Button
@ -360,7 +359,7 @@ function AllChatsContent({ workspaceId, className }: AllChatsContentProps) {
"h-auto w-full justify-start gap-2.5 overflow-hidden px-3 py-2.5 text-left text-base font-normal", "h-auto w-full justify-start gap-2.5 overflow-hidden px-3 py-2.5 text-left text-base font-normal",
"group-hover/item:bg-accent group-hover/item:text-accent-foreground", "group-hover/item:bg-accent group-hover/item:text-accent-foreground",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
thread.visibility === "SEARCH_SPACE" && "pr-16", thread.visibility === "SEARCH_SPACE" ? "pr-44" : "pr-36",
isActive && "bg-accent text-accent-foreground", isActive && "bg-accent text-accent-foreground",
isBusy && "opacity-50 pointer-events-none" isBusy && "opacity-50 pointer-events-none"
)} )}
@ -368,35 +367,24 @@ function AllChatsContent({ workspaceId, className }: AllChatsContentProps) {
<span className="min-w-0 flex-1 truncate">{thread.title || "New Chat"}</span> <span className="min-w-0 flex-1 truncate">{thread.title || "New Chat"}</span>
</Button> </Button>
) : ( ) : (
<Tooltip delayDuration={600}> <Button
<TooltipTrigger asChild> type="button"
<Button variant="ghost"
type="button" onClick={() => handleThreadClick(thread)}
variant="ghost" onMouseEnter={() => prefetchChatThread(thread.id)}
onClick={() => handleThreadClick(thread)} onFocus={() => prefetchChatThread(thread.id)}
onMouseEnter={() => prefetchChatThread(thread.id)} disabled={isBusy}
onFocus={() => prefetchChatThread(thread.id)} className={cn(
disabled={isBusy} "h-auto w-full justify-start gap-2.5 overflow-hidden px-3 py-2.5 text-left text-base font-normal",
className={cn( "group-hover/item:bg-accent group-hover/item:text-accent-foreground",
"h-auto w-full justify-start gap-2.5 overflow-hidden px-3 py-2.5 text-left text-base font-normal", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
"group-hover/item:bg-accent group-hover/item:text-accent-foreground", thread.visibility === "SEARCH_SPACE" ? "pr-44" : "pr-36",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", isActive && "bg-accent text-accent-foreground",
thread.visibility === "SEARCH_SPACE" && "pr-16", isBusy && "opacity-50 pointer-events-none"
isActive && "bg-accent text-accent-foreground", )}
isBusy && "opacity-50 pointer-events-none" >
)} <span className="min-w-0 flex-1 truncate">{thread.title || "New Chat"}</span>
> </Button>
<span className="min-w-0 flex-1 truncate">
{thread.title || "New Chat"}
</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" align="start">
<p>
{t("updated") || "Updated"}: {formatThreadTimestamp(thread.updatedAt)}
</p>
</TooltipContent>
</Tooltip>
)} )}
<div <div
@ -405,28 +393,30 @@ function AllChatsContent({ workspaceId, className }: AllChatsContentProps) {
isActive isActive
? "bg-gradient-to-l from-accent from-60% to-transparent" ? "bg-gradient-to-l from-accent from-60% to-transparent"
: "bg-gradient-to-l from-sidebar from-60% to-transparent group-hover/item:from-accent", : "bg-gradient-to-l from-sidebar from-60% to-transparent group-hover/item:from-accent",
isMobile "opacity-100"
? "opacity-0"
: thread.visibility === "SEARCH_SPACE" || openDropdownId === thread.id
? "opacity-100"
: "opacity-0 group-hover/item:opacity-100"
)} )}
> >
<div className="relative flex h-7 w-14 items-center justify-end"> <div className="relative flex h-7 w-40 items-center justify-end">
{thread.visibility === "SEARCH_SPACE" ? ( <div
<Badge className={cn(
variant="secondary" "absolute right-1 flex items-center justify-end gap-2 transition-opacity",
className={cn( openDropdownId === thread.id
"absolute right-0 h-5 shrink-0 rounded-sm border-0 bg-popover-foreground/10 px-1.5 text-[11px] text-popover-foreground transition-opacity hover:bg-popover-foreground/10", ? "opacity-0"
!isMobile && : "opacity-100 group-hover/item:opacity-0"
(openDropdownId === thread.id )}
? "opacity-0" >
: "opacity-100 group-hover/item:opacity-0") {thread.visibility === "SEARCH_SPACE" ? (
)} <Badge
> variant="secondary"
Shared className="h-5 shrink-0 rounded-sm border-0 bg-popover-foreground/10 px-1.5 text-[11px] text-popover-foreground hover:bg-popover-foreground/10"
</Badge> >
) : null} Shared
</Badge>
) : null}
<span className="whitespace-nowrap text-xs text-muted-foreground">
{formatRelativeDate(thread.updatedAt)}
</span>
</div>
<DropdownMenu <DropdownMenu
open={openDropdownId === thread.id} open={openDropdownId === thread.id}
onOpenChange={(isOpen) => setOpenDropdownId(isOpen ? thread.id : null)} onOpenChange={(isOpen) => setOpenDropdownId(isOpen ? thread.id : null)}
@ -436,11 +426,9 @@ function AllChatsContent({ workspaceId, className }: AllChatsContentProps) {
variant="ghost" variant="ghost"
size="icon" size="icon"
className={cn( className={cn(
"pointer-events-auto h-7 w-7 hover:bg-transparent", "pointer-events-auto absolute right-0 h-7 w-7 hover:bg-transparent",
openDropdownId === thread.id && "bg-accent hover:bg-accent", openDropdownId === thread.id && "bg-accent hover:bg-accent",
!isMobile && openDropdownId !== thread.id && "opacity-0 group-hover/item:opacity-100"
openDropdownId !== thread.id &&
"opacity-0 group-hover/item:opacity-100"
)} )}
disabled={isBusy} disabled={isBusy}
> >

View file

@ -5,29 +5,29 @@ import {
isThisYear, isThisYear,
isToday, isToday,
isTomorrow, isTomorrow,
isYesterday,
} from "date-fns"; } from "date-fns";
/** /**
* Format a date string as a human-readable relative time * Format a date string as a human-readable relative time
* - < 1 min: "Just now" * - < 1 min: "Just now"
* - < 60 min: "15m ago" * - < 60 min: "15 minutes ago"
* - Today: "Today, 2:30 PM" * - < 24 hours: "21 hours ago"
* - Yesterday: "Yesterday, 2:30 PM" * - < 7 days: "2 days ago"
* - < 7 days: "3d ago" * - Older this year: "Jan 15"
* - Older: "Jan 15, 2026" * - Older: "Jan 15, 2026"
*/ */
export function formatRelativeDate(dateString: string): string { export function formatRelativeDate(dateString: string): string {
const date = new Date(dateString); const date = new Date(dateString);
const now = new Date(); const now = new Date();
const minutesAgo = differenceInMinutes(now, date); const minutesAgo = Math.max(0, differenceInMinutes(now, date));
const daysAgo = differenceInDays(now, date); const hoursAgo = Math.floor(minutesAgo / 60);
const daysAgo = Math.floor(hoursAgo / 24);
if (minutesAgo < 1) return "Just now"; if (minutesAgo < 1) return "Just now";
if (minutesAgo < 60) return `${minutesAgo}m ago`; if (minutesAgo < 60) return `${minutesAgo} minute${minutesAgo === 1 ? "" : "s"} ago`;
if (isToday(date)) return `Today, ${format(date, "h:mm a")}`; if (hoursAgo < 24) return `${hoursAgo} hour${hoursAgo === 1 ? "" : "s"} ago`;
if (isYesterday(date)) return `Yesterday, ${format(date, "h:mm a")}`; if (daysAgo < 7) return `${daysAgo} day${daysAgo === 1 ? "" : "s"} ago`;
if (daysAgo < 7) return `${daysAgo}d ago`; if (isThisYear(date)) return format(date, "MMM d");
return format(date, "MMM d, yyyy"); return format(date, "MMM d, yyyy");
} }