wip: shared links ttl prototype

This commit is contained in:
Dustin Healy 2025-12-02 12:58:01 -08:00
parent 8bdc808074
commit 3d05d22e90
12 changed files with 234 additions and 13 deletions

View file

@ -6,6 +6,7 @@ import {
Spinner,
TooltipAnchor,
Label,
Dropdown,
OGDialogTemplate,
useToastContext,
} from '@librechat/client';
@ -17,6 +18,7 @@ import {
} from '~/data-provider';
import { NotificationSeverity } from '~/common';
import { useLocalize } from '~/hooks';
import { SHARE_EXPIRY } from '~/utils/shareExpiry';
export default function SharedLinkButton({
share,
@ -38,8 +40,12 @@ export default function SharedLinkButton({
const localize = useLocalize();
const { showToast } = useToastContext();
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [expiryLabel, setExpiryLabel] = useState(SHARE_EXPIRY.THIRTY_DAYS.label);
const shareId = share?.shareId ?? '';
const expirationOptions = Object.values(SHARE_EXPIRY);
const localizedOptions = expirationOptions.map((option) => localize(option.label));
const { mutateAsync: mutate, isLoading: isCreateLoading } = useCreateSharedLinkMutation({
onError: () => {
showToast({
@ -88,7 +94,9 @@ export default function SharedLinkButton({
};
const createShareLink = async () => {
const share = await mutate({ conversationId, targetMessageId });
const selectedOption = expirationOptions.find((option) => option.label === expiryLabel);
const expirationHours = selectedOption ? selectedOption.hours : undefined;
const share = await mutate({ conversationId, targetMessageId, expirationHours });
const newLink = generateShareLink(share.shareId);
setSharedLink(newLink);
};
@ -117,12 +125,33 @@ export default function SharedLinkButton({
return (
<>
<div className="flex gap-2">
<div className="flex flex-col gap-2">
{!shareId && (
<Button disabled={isCreateLoading} variant="submit" onClick={createShareLink}>
{!isCreateLoading && localize('com_ui_create_link')}
{isCreateLoading && <Spinner className="size-4" />}
</Button>
<>
<div className="flex items-center gap-2">
<Label htmlFor="expiry-select" className="text-sm">
{localize('com_ui_expires')}:
</Label>
<Dropdown
id="expiry-select"
label=""
value={localize(expiryLabel)}
onChange={(value) => {
const option = expirationOptions.find((opt) => localize(opt.label) === value);
if (option) {
setExpiryLabel(option.label);
}
}}
options={localizedOptions}
sizeClasses="flex-1"
portal={false}
/>
</div>
<Button disabled={isCreateLoading} variant="submit" onClick={createShareLink}>
{!isCreateLoading && localize('com_ui_create_link')}
{isCreateLoading && <Spinner className="size-4" />}
</Button>
</>
)}
{shareId && (
<div className="flex items-center gap-2">

View file

@ -232,6 +232,28 @@ export default function SharedLinks() {
mobileSize: '20%',
},
},
{
accessorKey: 'expiresAt',
header: () => <Label className="px-2 text-xs sm:text-sm">{localize('com_ui_expires')}</Label>,
cell: ({ row }) => {
const expiresAt = row.original.expiresAt;
if (!expiresAt) {
return <span className="text-xs text-text-secondary sm:text-sm">{localize('com_ui_never')}</span>;
}
const expiryDate = new Date(expiresAt);
const isExpired = expiryDate < new Date();
return (
<span className={`text-xs sm:text-sm ${isExpired ? 'text-red-500' : 'text-text-secondary'}`}>
{isExpired ? `${localize('com_ui_expired')} ` : ''}
{formatDate(expiresAt.toString(), isSmallScreen)}
</span>
);
},
meta: {
size: '10%',
mobileSize: '20%',
},
},
{
accessorKey: 'actions',
header: () => (

View file

@ -170,24 +170,32 @@ export const useArchiveConvoMutation = (
export const useCreateSharedLinkMutation = (
options?: t.MutationOptions<
t.TCreateShareLinkRequest,
{ conversationId: string; targetMessageId?: string }
{ conversationId: string; targetMessageId?: string; expirationHours?: number }
>,
): UseMutationResult<
t.TSharedLinkResponse,
unknown,
{ conversationId: string; targetMessageId?: string },
{ conversationId: string; targetMessageId?: string; expirationHours?: number },
unknown
> => {
const queryClient = useQueryClient();
const { onSuccess, ..._options } = options || {};
return useMutation(
({ conversationId, targetMessageId }: { conversationId: string; targetMessageId?: string }) => {
({
conversationId,
targetMessageId,
expirationHours,
}: {
conversationId: string;
targetMessageId?: string;
expirationHours?: number;
}) => {
if (!conversationId) {
throw new Error('Conversation ID is required');
}
return dataService.createSharedLink(conversationId, targetMessageId);
return dataService.createSharedLink(conversationId, targetMessageId, expirationHours);
},
{
onSuccess: (_data: t.TSharedLinkResponse, vars, context) => {

View file

@ -900,6 +900,13 @@
"com_ui_everyone_permission_level": "Everyone's permission level",
"com_ui_examples": "Examples",
"com_ui_expand_chat": "Expand Chat",
"com_ui_expired": "Expired",
"com_ui_expires": "Expires",
"com_ui_expiry_hour": "Hour",
"com_ui_expiry_12_hours": "12 Hours",
"com_ui_expiry_day": "Day",
"com_ui_expiry_week": "Week",
"com_ui_expiry_month": "Month",
"com_ui_export_convo_modal": "Export Conversation Modal",
"com_ui_feedback_more": "More...",
"com_ui_feedback_more_information": "Provide additional feedback",
@ -1049,6 +1056,7 @@
"com_ui_more_info": "More info",
"com_ui_my_prompts": "My Prompts",
"com_ui_name": "Name",
"com_ui_never": "Never",
"com_ui_new": "New",
"com_ui_new_chat": "New chat",
"com_ui_new_conversation_title": "New Conversation Title",

View file

@ -0,0 +1,32 @@
export const SHARE_EXPIRY = {
ONE_HOUR: {
label: 'com_ui_expiry_hour',
value: 60 * 60 * 1000, // 1 hour in ms
hours: 1,
},
TWELVE_HOURS: {
label: 'com_ui_expiry_12_hours',
value: 12 * 60 * 60 * 1000,
hours: 12,
},
ONE_DAY: {
label: 'com_ui_expiry_day',
value: 24 * 60 * 60 * 1000,
hours: 24,
},
SEVEN_DAYS: {
label: 'com_ui_expiry_week',
value: 7 * 24 * 60 * 60 * 1000,
hours: 168,
},
THIRTY_DAYS: {
label: 'com_ui_expiry_month',
value: 30 * 24 * 60 * 60 * 1000,
hours: 720,
},
NEVER: {
label: 'com_ui_never',
value: 0,
hours: 0,
},
} as const;