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

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
403 changes: 247 additions & 156 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
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
110 changes: 44 additions & 66 deletions client/src/components/Conversations/ConvoOptions/ShareButton.tsx
Original file line number Diff line number Diff line change
@@ -1,112 +1,90 @@
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 { useGetSharedLinkQuery } from 'librechat-data-provider/react-query';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import SharedLinkButton from './SharedLinkButton';
import { NotificationSeverity } from '~/common';
import { Spinner } from '~/components/svg';
import { Spinner, OGDialog } from '~/components';
import { useLocalize } from '~/hooks';

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 { data: share, isLoading } = useGetSharedLinkQuery(conversationId);
const [sharedLink, setSharedLink] = useState('');
const [showQR, setShowQR] = useState(false);

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
}, []);

if (!conversationId) {
return null;
}

const buttons = share && (
<SharedLinkButton
share={share}
conversationId={conversationId}
setShare={setShare}
isUpdated={isUpdated}
setIsUpdated={setIsUpdated}
/>
);
const button =
isLoading === true ? null : (
<SharedLinkButton
share={share}
conversationId={conversationId}
setShareDialogOpen={onOpenChange}
showQR={showQR}
setShowQR={setShowQR}
sharedLink={sharedLink}
setSharedLink={setSharedLink}
/>
);

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');
}
// 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>
)}

{share?.shareId !== null && (
<div className="cursor-text break-all text-center text-sm text-text-secondary">
{sharedLink}
</div>
)}
</div>
</div>
}
/>
Expand Down
Loading
Loading