Add globe indicator for chats with public links

This commit is contained in:
CREDO23 2026-02-04 18:26:38 +02:00
parent f5aa520743
commit fb371d09f5

View file

@ -1,8 +1,9 @@
"use client";
import { useQueryClient } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useAtomValue, useSetAtom } from "jotai";
import { Globe, User, Users } from "lucide-react";
import { useParams, useRouter } from "next/navigation";
import { useCallback, useMemo, useState } from "react";
import { toast } from "sonner";
import { currentThreadAtom, setThreadVisibilityAtom } from "@/atoms/chat/current-thread.atom";
@ -11,6 +12,7 @@ import { createPublicChatSnapshotMutationAtom } from "@/atoms/public-chat-snapsh
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { chatThreadsApiService } from "@/lib/apis/chat-threads-api.service";
import {
type ChatVisibility,
type ThreadRecord,
@ -46,6 +48,8 @@ const visibilityOptions: {
export function ChatShareButton({ thread, onVisibilityChange, className }: ChatShareButtonProps) {
const queryClient = useQueryClient();
const router = useRouter();
const params = useParams();
const [open, setOpen] = useState(false);
// Use Jotai atom for visibility (single source of truth)
@ -65,6 +69,16 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
return access.permissions?.includes("public_sharing:create") ?? false;
}, [access]);
// Query to check if thread has public snapshots
const { data: snapshotsData } = useQuery({
queryKey: ["thread-snapshots", thread?.id],
queryFn: () => chatThreadsApiService.listPublicChatSnapshots({ thread_id: thread!.id }),
enabled: !!thread?.id,
staleTime: 30000, // Cache for 30 seconds
});
const hasPublicSnapshots = (snapshotsData?.snapshots?.length ?? 0) > 0;
const snapshotCount = snapshotsData?.snapshots?.length ?? 0;
// Use Jotai visibility if available (synced from chat page), otherwise fall back to thread prop
const currentVisibility = currentThreadState.visibility ?? thread?.visibility ?? "PRIVATE";
@ -106,11 +120,13 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
try {
await createSnapshot({ thread_id: thread.id });
// Refetch snapshots to show the globe indicator
await queryClient.invalidateQueries({ queryKey: ["thread-snapshots", thread.id] });
setOpen(false);
} catch (error) {
console.error("Failed to create public link:", error);
}
}, [thread, createSnapshot]);
}, [thread, createSnapshot, queryClient]);
// Don't show if no thread (new chat that hasn't been created yet)
if (!thread) {
@ -121,112 +137,131 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
const buttonLabel = currentVisibility === "PRIVATE" ? "Private" : "Shared";
return (
<Popover open={open} onOpenChange={setOpen}>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button
variant="outline"
size="icon"
className={cn(
"h-8 w-8 md:w-auto md:px-3 md:gap-2 relative bg-muted hover:bg-muted/80 border-0",
className
)}
>
<CurrentIcon className="h-4 w-4" />
<span className="hidden md:inline text-sm">{buttonLabel}</span>
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>Share settings</TooltipContent>
</Tooltip>
<PopoverContent
className="w-[280px] md:w-[320px] p-0 rounded-lg shadow-lg border-border/60"
align="end"
sideOffset={8}
onCloseAutoFocus={(e) => e.preventDefault()}
>
<div className="p-1.5 space-y-1">
{/* Visibility Options */}
{visibilityOptions.map((option) => {
const isSelected = currentVisibility === option.value;
const Icon = option.icon;
return (
<button
type="button"
key={option.value}
onClick={() => handleVisibilityChange(option.value)}
className={cn(
"w-full flex items-center gap-2.5 px-2.5 py-2 rounded-md transition-all",
"hover:bg-accent/50 cursor-pointer",
"focus:outline-none",
isSelected && "bg-accent/80"
)}
<div className={cn("flex items-center gap-1", className)}>
<Popover open={open} onOpenChange={setOpen}>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button
variant="outline"
size="icon"
className="h-8 w-8 md:w-auto md:px-3 md:gap-2 relative bg-muted hover:bg-muted/80 border-0"
>
<div
<CurrentIcon className="h-4 w-4" />
<span className="hidden md:inline text-sm">{buttonLabel}</span>
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>Share settings</TooltipContent>
</Tooltip>
<PopoverContent
className="w-[280px] md:w-[320px] p-0 rounded-lg shadow-lg border-border/60"
align="end"
sideOffset={8}
onCloseAutoFocus={(e) => e.preventDefault()}
>
<div className="p-1.5 space-y-1">
{/* Visibility Options */}
{visibilityOptions.map((option) => {
const isSelected = currentVisibility === option.value;
const Icon = option.icon;
return (
<button
type="button"
key={option.value}
onClick={() => handleVisibilityChange(option.value)}
className={cn(
"size-7 rounded-md shrink-0 grid place-items-center",
isSelected ? "bg-primary/10" : "bg-muted"
"w-full flex items-center gap-2.5 px-2.5 py-2 rounded-md transition-all",
"hover:bg-accent/50 cursor-pointer",
"focus:outline-none",
isSelected && "bg-accent/80"
)}
>
<Icon
<div
className={cn(
"size-4 block",
isSelected ? "text-primary" : "text-muted-foreground"
"size-7 rounded-md shrink-0 grid place-items-center",
isSelected ? "bg-primary/10" : "bg-muted"
)}
/>
</div>
<div className="flex-1 text-left min-w-0">
<div className="flex items-center gap-1.5">
<span className={cn("text-sm font-medium", isSelected && "text-primary")}>
{option.label}
</span>
>
<Icon
className={cn(
"size-4 block",
isSelected ? "text-primary" : "text-muted-foreground"
)}
/>
</div>
<p className="text-xs text-muted-foreground mt-0.5 leading-snug">
{option.description}
</p>
</div>
</button>
);
})}
{canCreatePublicLink && (
<>
{/* Divider */}
<div className="border-t border-border my-1" />
{/* Public Link Option */}
<button
type="button"
onClick={handleCreatePublicLink}
disabled={isCreatingSnapshot}
className={cn(
"w-full flex items-center gap-2.5 px-2.5 py-2 rounded-md transition-all",
"hover:bg-accent/50 cursor-pointer",
"focus:outline-none",
"disabled:opacity-50 disabled:cursor-not-allowed"
)}
>
<div className="size-7 rounded-md shrink-0 grid place-items-center bg-muted">
<Globe className="size-4 block text-muted-foreground" />
</div>
<div className="flex-1 text-left min-w-0">
<div className="flex items-center gap-1.5">
<span className="text-sm font-medium">
{isCreatingSnapshot ? "Creating link..." : "Create public link"}
</span>
<div className="flex-1 text-left min-w-0">
<div className="flex items-center gap-1.5">
<span className={cn("text-sm font-medium", isSelected && "text-primary")}>
{option.label}
</span>
</div>
<p className="text-xs text-muted-foreground mt-0.5 leading-snug">
{option.description}
</p>
</div>
<p className="text-xs text-muted-foreground mt-0.5 leading-snug">
Creates a shareable snapshot of this chat
</p>
</div>
</button>
</>
)}
</div>
</PopoverContent>
</Popover>
</button>
);
})}
{canCreatePublicLink && (
<>
{/* Divider */}
<div className="border-t border-border my-1" />
{/* Public Link Option */}
<button
type="button"
onClick={handleCreatePublicLink}
disabled={isCreatingSnapshot}
className={cn(
"w-full flex items-center gap-2.5 px-2.5 py-2 rounded-md transition-all",
"hover:bg-accent/50 cursor-pointer",
"focus:outline-none",
"disabled:opacity-50 disabled:cursor-not-allowed"
)}
>
<div className="size-7 rounded-md shrink-0 grid place-items-center bg-muted">
<Globe className="size-4 block text-muted-foreground" />
</div>
<div className="flex-1 text-left min-w-0">
<div className="flex items-center gap-1.5">
<span className="text-sm font-medium">
{isCreatingSnapshot ? "Creating link..." : "Create public link"}
</span>
</div>
<p className="text-xs text-muted-foreground mt-0.5 leading-snug">
Creates a shareable snapshot of this chat
</p>
</div>
</button>
</>
)}
</div>
</PopoverContent>
</Popover>
{/* Globe indicator when public snapshots exist - clicks to settings */}
{hasPublicSnapshots && (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => router.push(`/dashboard/${params.search_space_id}/settings`)}
className="flex items-center justify-center h-8 w-8 rounded-md bg-muted/50 hover:bg-muted transition-colors"
>
<Globe className="h-4 w-4 text-muted-foreground" />
</button>
</TooltipTrigger>
<TooltipContent>
{snapshotCount === 1
? "This chat has a public link - Click to manage"
: `This chat has ${snapshotCount} public links - Click to manage`}
</TooltipContent>
</Tooltip>
)}
</div>
);
}