Skip to content

Commit

Permalink
🔗 feat: Enhance Share Functionality, Optimize DataTable & Fix Critica…
Browse files Browse the repository at this point in the history
…l Bugs (#5220)

* 🔄 refactor: frontend and backend share link logic; feat: qrcode for share link; feat: refresh link

* 🐛 fix: Conditionally render shared link and refactor share link creation logic

* 🐛 fix: Correct conditional check for shareId in ShareButton component

* 🔄 refactor: Update shared links API and data handling; improve query parameters and response structure

* 🔄 refactor: Update shared links pagination and response structure; replace pageNumber with cursor for improved data fetching

* 🔄 refactor: DataTable performance optimization

* fix: delete shared link cache update

* 🔄 refactor: Enhance shared links functionality; add conversationId to shared link model and update related components

* 🔄 refactor: Add delete functionality to SharedLinkButton; integrate delete mutation and confirmation dialog

* 🔄 feat: Add AnimatedSearchInput component with gradient animations and search functionality; update search handling in API and localization

* 🔄 refactor: Improve SharedLinks component; enhance delete functionality and loading states, optimize AnimatedSearchInput, and refine DataTable scrolling behavior

* fix: mutation type issues with deleted shared link mutation

* fix: MutationOptions types

* fix: Ensure only public shared links are retrieved in getSharedLink function

* fix: `qrcode.react` install location

* fix: ensure non-public shared links are not fetched when checking for existing shared links, and remove deprecated .exec() method for queries

* fix: types and import order

* refactor: cleanup share button UI logic, make more intuitive

---------

Co-authored-by: Danny Avila <[email protected]>
  • Loading branch information
berry-13 and danny-avila authored Jan 21, 2025
1 parent 460cde0 commit fa9e778
Show file tree
Hide file tree
Showing 55 changed files with 1,747 additions and 1,943 deletions.
398 changes: 243 additions & 155 deletions api/models/Share.js

Large diffs are not rendered by default.

8 changes: 0 additions & 8 deletions api/models/schema/shareSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,6 @@ const shareSchema = mongoose.Schema(
index: true,
},
isPublic: {
type: Boolean,
default: false,
},
isVisible: {
type: Boolean,
default: false,
},
isAnonymous: {
type: Boolean,
default: true,
},
Expand Down
83 changes: 58 additions & 25 deletions api/server/routes/share.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const express = require('express');

const {
getSharedLink,
getSharedMessages,
createSharedLink,
updateSharedLink,
Expand Down Expand Up @@ -45,29 +46,60 @@ if (allowSharedLinks) {
*/
router.get('/', requireJwtAuth, async (req, res) => {
try {
let pageNumber = req.query.pageNumber || 1;
pageNumber = parseInt(pageNumber, 10);
const params = {
pageParam: req.query.cursor,
pageSize: Math.max(1, parseInt(req.query.pageSize) || 10),
isPublic: isEnabled(req.query.isPublic),
sortBy: ['createdAt', 'title'].includes(req.query.sortBy) ? req.query.sortBy : 'createdAt',
sortDirection: ['asc', 'desc'].includes(req.query.sortDirection)
? req.query.sortDirection
: 'desc',
search: req.query.search
? decodeURIComponent(req.query.search.trim())
: undefined,
};

if (isNaN(pageNumber) || pageNumber < 1) {
return res.status(400).json({ error: 'Invalid page number' });
}
const result = await getSharedLinks(
req.user.id,
params.pageParam,
params.pageSize,
params.isPublic,
params.sortBy,
params.sortDirection,
params.search,
);

let pageSize = req.query.pageSize || 25;
pageSize = parseInt(pageSize, 10);
res.status(200).send({
links: result.links,
nextCursor: result.nextCursor,
hasNextPage: result.hasNextPage,
});
} catch (error) {
console.error('Error getting shared links:', error);
res.status(500).json({
message: 'Error getting shared links',
error: error.message,
});
}
});

if (isNaN(pageSize) || pageSize < 1) {
return res.status(400).json({ error: 'Invalid page size' });
}
const isPublic = req.query.isPublic === 'true';
res.status(200).send(await getSharedLinks(req.user.id, pageNumber, pageSize, isPublic));
router.get('/link/:conversationId', requireJwtAuth, async (req, res) => {
try {
const share = await getSharedLink(req.user.id, req.params.conversationId);

return res.status(200).json({
success: share.success,
shareId: share.shareId,
conversationId: req.params.conversationId,
});
} catch (error) {
res.status(500).json({ message: 'Error getting shared links' });
res.status(500).json({ message: 'Error getting shared link' });
}
});

router.post('/', requireJwtAuth, async (req, res) => {
router.post('/:conversationId', requireJwtAuth, async (req, res) => {
try {
const created = await createSharedLink(req.user.id, req.body);
const created = await createSharedLink(req.user.id, req.params.conversationId);
if (created) {
res.status(200).json(created);
} else {
Expand All @@ -78,11 +110,11 @@ router.post('/', requireJwtAuth, async (req, res) => {
}
});

router.patch('/', requireJwtAuth, async (req, res) => {
router.patch('/:shareId', requireJwtAuth, async (req, res) => {
try {
const updated = await updateSharedLink(req.user.id, req.body);
if (updated) {
res.status(200).json(updated);
const updatedShare = await updateSharedLink(req.user.id, req.params.shareId);
if (updatedShare) {
res.status(200).json(updatedShare);
} else {
res.status(404).end();
}
Expand All @@ -93,14 +125,15 @@ router.patch('/', requireJwtAuth, async (req, res) => {

router.delete('/:shareId', requireJwtAuth, async (req, res) => {
try {
const deleted = await deleteSharedLink(req.user.id, { shareId: req.params.shareId });
if (deleted) {
res.status(200).json(deleted);
} else {
res.status(404).end();
const result = await deleteSharedLink(req.user.id, req.params.shareId);

if (!result) {
return res.status(404).json({ message: 'Share not found' });
}

return res.status(200).json(result);
} catch (error) {
res.status(500).json({ message: 'Error deleting shared link' });
return res.status(400).json({ message: error.message });
}
});

Expand Down
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"lucide-react": "^0.394.0",
"match-sorter": "^6.3.4",
"msedge-tts": "^1.3.4",
"qrcode.react": "^4.2.0",
"rc-input-number": "^7.4.2",
"react": "^18.2.0",
"react-avatar-editor": "^13.0.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ function ConvoOptions({
/>
{showShareDialog && (
<ShareButton
title={title ?? ''}
conversationId={conversationId ?? ''}
open={showShareDialog}
onOpenChange={setShowShareDialog}
Expand Down
124 changes: 57 additions & 67 deletions client/src/components/Conversations/ConvoOptions/ShareButton.tsx
Original file line number Diff line number Diff line change
@@ -1,112 +1,102 @@
import React, { useState, useEffect } from 'react';
import { OGDialog } from '~/components/ui';
import { useToastContext } from '~/Providers';
import type { TSharedLink } from 'librechat-data-provider';
import { useCreateSharedLinkMutation } from '~/data-provider';
import { QRCodeSVG } from 'qrcode.react';
import { Copy, CopyCheck } from 'lucide-react';
import { useGetSharedLinkQuery } from 'librechat-data-provider/react-query';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useLocalize, useCopyToClipboard } from '~/hooks';
import { Button, Spinner, OGDialog } from '~/components';
import SharedLinkButton from './SharedLinkButton';
import { NotificationSeverity } from '~/common';
import { Spinner } from '~/components/svg';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';

export default function ShareButton({
conversationId,
title,
open,
onOpenChange,
triggerRef,
children,
}: {
conversationId: string;
title: string;
open: boolean;
onOpenChange: React.Dispatch<React.SetStateAction<boolean>>;
triggerRef?: React.RefObject<HTMLButtonElement>;
children?: React.ReactNode;
}) {
const localize = useLocalize();
const { showToast } = useToastContext();
const { mutate, isLoading } = useCreateSharedLinkMutation();
const [share, setShare] = useState<TSharedLink | null>(null);
const [isUpdated, setIsUpdated] = useState(false);
const [isNewSharedLink, setIsNewSharedLink] = useState(false);
const [showQR, setShowQR] = useState(false);
const [sharedLink, setSharedLink] = useState('');
const [isCopying, setIsCopying] = useState(false);
const { data: share, isLoading } = useGetSharedLinkQuery(conversationId);
const copyLink = useCopyToClipboard({ text: sharedLink });

useEffect(() => {
if (!open && triggerRef && triggerRef.current) {
triggerRef.current.focus();
if (share?.shareId !== undefined) {
const link = `${window.location.protocol}//${window.location.host}/share/${share.shareId}`;
setSharedLink(link);
}
}, [open, triggerRef]);
}, [share]);

useEffect(() => {
if (isLoading || share) {
return;
}
const data = {
conversationId,
title,
isAnonymous: true,
};

mutate(data, {
onSuccess: (result) => {
setShare(result);
setIsNewSharedLink(!result.isPublic);
},
onError: () => {
showToast({
message: localize('com_ui_share_error'),
severity: NotificationSeverity.ERROR,
showIcon: true,
});
},
});

// mutation.mutate should only be called once
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const button =
isLoading === true ? null : (
<SharedLinkButton
share={share}
conversationId={conversationId}
setShareDialogOpen={onOpenChange}
showQR={showQR}
setShowQR={setShowQR}
setSharedLink={setSharedLink}
/>
);

if (!conversationId) {
return null;
}

const buttons = share && (
<SharedLinkButton
share={share}
conversationId={conversationId}
setShare={setShare}
isUpdated={isUpdated}
setIsUpdated={setIsUpdated}
/>
);
const shareId = share?.shareId ?? '';

return (
<OGDialog open={open} onOpenChange={onOpenChange} triggerRef={triggerRef}>
{children}
<OGDialogTemplate
buttons={buttons}
buttons={button}
showCloseButton={true}
showCancelButton={false}
title={localize('com_ui_share_link_to_chat')}
className="max-w-[550px]"
main={
<div>
<div className="h-full py-2 text-gray-400 dark:text-gray-200">
<div className="h-full py-2 text-text-primary">
{(() => {
if (isLoading) {
if (isLoading === true) {
return <Spinner className="m-auto h-14 animate-spin" />;
}

if (isUpdated) {
return isNewSharedLink
? localize('com_ui_share_created_message')
: localize('com_ui_share_updated_message');
}

return share?.isPublic === true
return share?.success === true
? localize('com_ui_share_update_message')
: localize('com_ui_share_create_message');
})()}
</div>
<div className="relative items-center rounded-lg p-2">
{showQR && (
<div className="mb-4 flex flex-col items-center">
<QRCodeSVG value={sharedLink} size={200} marginSize={2} className="rounded-2xl" />
</div>
)}

{shareId && (
<div className="flex items-center gap-2 rounded-md bg-surface-secondary p-2">
<div className="flex-1 break-all text-sm text-text-secondary">{sharedLink}</div>
<Button
size="sm"
variant="outline"
onClick={() => {
if (isCopying) {
return;
}
copyLink(setIsCopying);
}}
className={cn('shrink-0', isCopying ? 'cursor-default' : '')}
>
{isCopying ? <CopyCheck className="size-4" /> : <Copy className="size-4" />}
</Button>
</div>
)}
</div>
</div>
}
/>
Expand Down
Loading

0 comments on commit fa9e778

Please sign in to comment.