Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

πŸ”— feat: Enhance Share Functionality, Optimize DataTable & Fix Critical Bugs #5220

Merged
merged 18 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
77b9477
πŸ”„ refactor: frontend and backend share link logic; feat: qrcode for s…
berry-13 Jan 5, 2025
7720215
πŸ› fix: Conditionally render shared link and refactor share link creat…
berry-13 Jan 5, 2025
a9a3ec5
πŸ› fix: Correct conditional check for shareId in ShareButton component
berry-13 Jan 5, 2025
bb15103
πŸ”„ refactor: Update shared links API and data handling; improve query …
berry-13 Jan 6, 2025
8d40dc3
πŸ”„ refactor: Update shared links pagination and response structure; re…
berry-13 Jan 8, 2025
c009af8
πŸ”„ refactor: DataTable performance optimization
berry-13 Jan 11, 2025
63f6013
fix: delete shared link cache update
berry-13 Jan 12, 2025
d0c177f
πŸ”„ refactor: Enhance shared links functionality; add conversationId to…
berry-13 Jan 12, 2025
4e90f61
πŸ”„ refactor: Add delete functionality to SharedLinkButton; integrate d…
berry-13 Jan 12, 2025
3e82549
πŸ”„ feat: Add AnimatedSearchInput component with gradient animations an…
berry-13 Jan 15, 2025
098c155
πŸ”„ refactor: Improve SharedLinks component; enhance delete functionali…
berry-13 Jan 15, 2025
82fa32e
fix: mutation type issues with deleted shared link mutation
danny-avila Jan 20, 2025
08a92f7
fix: MutationOptions types
danny-avila Jan 20, 2025
f114756
fix: Ensure only public shared links are retrieved in getSharedLink f…
berry-13 Jan 20, 2025
1ecfaf3
fix: `qrcode.react` install location
danny-avila Jan 21, 2025
31aaff6
fix: ensure non-public shared links are not fetched when checking for…
danny-avila Jan 21, 2025
e61715d
fix: types and import order
danny-avila Jan 21, 2025
3c1d80b
refactor: cleanup share button UI logic, make more intuitive
danny-avila Jan 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading