mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-26 23:51:14 +02:00
refactor(AllChatsSidebar): replace timestamp formatting with relative date display and adjust button styles
This commit is contained in:
parent
7889babb8e
commit
a466fdcf5d
2 changed files with 56 additions and 68 deletions
|
|
@ -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}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue