chat / client /src /components /Nav /SettingsTabs /Data /SharedLinkTable.tsx
Zhitomir's picture
chat init
aa2e6af
Raw
History Blame Contribute Delete
6.41 kB
import { useMemo, useState, MouseEvent } from 'react';
import { Link } from 'react-router-dom';
import { MessageSquare, Link as LinkIcon } from 'lucide-react';
import type { SharedLinksResponse, TSharedLink } from 'librechat-data-provider';
import { useDeleteSharedLinkMutation, useSharedLinksInfiniteQuery } from '~/data-provider';
import { useAuthContext, useLocalize, useNavScrolling } from '~/hooks';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import { cn } from '~/utils';
import {
Spinner,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
TrashIcon,
} from '~/components';
function SharedLinkDeleteButton({
shareId,
setIsDeleting,
}: {
shareId: string;
setIsDeleting: (isDeleting: boolean) => void;
}) {
const localize = useLocalize();
const { showToast } = useToastContext();
const mutation = useDeleteSharedLinkMutation({
onError: () => {
showToast({
message: localize('com_ui_share_delete_error'),
severity: NotificationSeverity.ERROR,
showIcon: true,
});
setIsDeleting(false);
},
});
const handleDelete = async (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (mutation.isLoading) {
return;
}
setIsDeleting(true);
await mutation.mutateAsync({ shareId });
setIsDeleting(false);
};
return (
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<span onClick={handleDelete}>
<TrashIcon />
</span>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={0}>
{localize('com_ui_delete')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
function SourceChatButton({ conversationId }: { conversationId: string }) {
const localize = useLocalize();
return (
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<Link to={`/c/${conversationId}`} target="_blank" rel="noreferrer">
<MessageSquare className="h-4 w-4 hover:text-gray-300" />
</Link>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={0}>
{localize('com_nav_source_chat')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
function ShareLinkRow({ sharedLink }: { sharedLink: TSharedLink }) {
const [isDeleting, setIsDeleting] = useState(false);
return (
<tr
key={sharedLink.conversationId}
className="border-b border-gray-200 text-sm font-normal dark:border-white/10"
>
<td
className={cn(
'flex items-center py-3 text-blue-800/70 dark:text-blue-500',
isDeleting && 'opacity-50',
)}
>
<Link to={`/share/${sharedLink.shareId}`} target="_blank" rel="noreferrer" className="flex">
<LinkIcon className="mr-1 h-5 w-5" />
{sharedLink.title}
</Link>
</td>
<td className="p-3">
<div className="flex justify-between">
<div className={cn('flex justify-start dark:text-gray-200', isDeleting && 'opacity-50')}>
{new Date(sharedLink.createdAt).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
})}
</div>
<div
className={cn(
'flex items-center justify-end gap-3 text-gray-400',
isDeleting && 'opacity-50',
)}
>
{sharedLink.conversationId && (
<>
<SourceChatButton conversationId={sharedLink.conversationId} />
<div className={cn('h-4 w-4 cursor-pointer', !isDeleting && 'hover:text-gray-300')}>
<SharedLinkDeleteButton
shareId={sharedLink.shareId}
setIsDeleting={setIsDeleting}
/>
</div>
</>
)}
</div>
</div>
</td>
</tr>
);
}
export default function ShareLinkTable({ className }: { className?: string }) {
const localize = useLocalize();
const { isAuthenticated } = useAuthContext();
const [showLoading, setShowLoading] = useState(false);
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isError, isLoading } =
useSharedLinksInfiniteQuery({ pageNumber: '1', isPublic: true }, { enabled: isAuthenticated });
const { containerRef } = useNavScrolling<SharedLinksResponse>({
setShowLoading,
hasNextPage: hasNextPage,
fetchNextPage: fetchNextPage,
isFetchingNextPage: isFetchingNextPage,
});
const sharedLinks = useMemo(() => data?.pages.flatMap((page) => page.sharedLinks) || [], [data]);
const classProp: { className?: string } = {
className: 'p-1 hover:text-black dark:hover:text-white',
};
if (className) {
classProp.className = className;
}
if (isLoading) {
return <Spinner className="m-1 mx-auto mb-4 h-4 w-4 text-black dark:text-white" />;
}
if (isError) {
return (
<div className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200">
{localize('com_ui_share_retrieve_error')}
</div>
);
}
if (!sharedLinks || sharedLinks.length === 0) {
return <div className="text-gray-300">{localize('com_nav_shared_links_empty')}</div>;
}
return (
<div
className={cn(
'grid w-full gap-2',
'-mr-2 flex-1 flex-col overflow-y-auto pr-2 transition-opacity duration-500',
'max-h-[350px]',
)}
ref={containerRef}
>
<table className="table-fixed text-left">
<thead className="sticky top-0 bg-white dark:bg-gray-700">
<tr className="border-b border-gray-200 text-sm font-semibold text-gray-500 dark:border-white/10 dark:text-gray-200">
<th className="p-3">{localize('com_nav_shared_links_name')}</th>
<th className="p-3">{localize('com_nav_shared_links_date_shared')}</th>
</tr>
</thead>
<tbody>
{sharedLinks.map((sharedLink) => (
<ShareLinkRow key={sharedLink.shareId} sharedLink={sharedLink} />
))}
</tbody>
</table>
{(isFetchingNextPage || showLoading) && (
<Spinner className={cn('m-1 mx-auto mb-4 h-4 w-4 text-black dark:text-white')} />
)}
</div>
);
}