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: shared links #2659

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d034a9d
✨ feat(types): add necessary types for shared link feature
ohneda May 9, 2024
6163e9a
✨ feat: add shared links functions to data service
ohneda May 9, 2024
043cd14
✨ feat: Add useGetSharedMessages hook to fetch shared messages by sha…
ohneda May 9, 2024
737610d
✨ feat: Add share schema and data access functions to API models
ohneda May 9, 2024
17efd23
✨ feat: Add share endpoint to API
ohneda May 9, 2024
bfd83af
♻️ refactor(utils): generalize react-query cache manipulation functions
ohneda May 9, 2024
c8773db
✨ feat(shared-link): add functions to manipulate shared link cache list
ohneda May 9, 2024
a60ef1d
✨ feat: Add mutations and queries for shared links
ohneda May 9, 2024
a3b863f
✨ feat(shared-link): add `Share` button to conversation list
ohneda May 10, 2024
735072c
♻️ refactor(hooks): generalize useNavScrolling for broader use
ohneda May 10, 2024
43b35d1
✨ feat(settings): add shared links listing table with delete function…
ohneda May 10, 2024
929fd6d
♻️ refactor(components): separate `EndpointIcon` from `Icon` componen…
ohneda May 10, 2024
c1d820e
♻️ refactor: update useGetSharedMessages to return TSharedLink
ohneda May 10, 2024
8ab12e3
✨ feat(shared link): add UI for displaying shared conversations witho…
ohneda May 10, 2024
1899aee
🔧 chore: Add translations
ohneda May 10, 2024
fc9243c
Merge branch 'main' of github.com:danny-avila/LibreChat into feature/…
ohneda May 10, 2024
d6ae157
Merge branch 'main' of github.com:danny-avila/LibreChat into feature/…
ohneda May 10, 2024
49e3c2f
♻️ refactor: add icon and tooltip props to EditMenuButton component
ohneda May 14, 2024
000541a
♻️irefactor: added DropdownMenu for Export and Share
ohneda May 14, 2024
81003aa
Merge branch 'main' of github.com:danny-avila/LibreChat into feature/…
ohneda May 14, 2024
a021a62
♻️ refactor: renamed component names more intuitive
ohneda May 14, 2024
3fa7089
🌍 chore: updated translations
ohneda May 14, 2024
6cd5655
Merge branch 'main' of github.com:danny-avila/LibreChat into feature/…
ohneda May 14, 2024
38a6c94
🚑 fix: ensure error messages are displayed on the conversation when t…
ohneda May 14, 2024
d77244e
Merge branch 'main' into feature/shared-link
ohneda May 14, 2024
e8a4cc8
refactor: Login form improvement
ohneda May 15, 2024
1f65222
Merge branch 'main' into feature/shared-link
ohneda May 16, 2024
decc903
Merge branch 'main' into feature/shared-link
ohneda May 16, 2024
0950d24
Merge branch 'feature/shared-link' of github.com:ohneda/LibreChat int…
ohneda May 17, 2024
676ac16
Merge branch 'main' of github.com:danny-avila/LibreChat into feature/…
ohneda May 17, 2024
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
91 changes: 91 additions & 0 deletions api/models/Share.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
const crypto = require('crypto');
const { getMessages } = require('./Message');
const SharedLink = require('./schema/shareSchema');
const logger = require('~/config/winston');

module.exports = {
SharedLink,
getSharedMessages: async (shareId) => {
try {
const share = await SharedLink.findOne({ shareId }).populate('messages').lean();
if (!share || !share.conversationId || !share.isPublic) {
return null;
}

const anonymousMessage = share.messages.map((message) => {
if (share.isAnonymous) {
return {
...message,
user: 'anonymous',
};
}
return message;
});
return {
...share,
messages: anonymousMessage,
};
} catch (error) {
logger.error('[getShare] Error getting share link', error);
return { message: 'Error getting share link' };
}
},

getSharedLinks: async (user, pageNumber = 1, pageSize = 25, isPublic = true) => {
const query = { user, isPublic };
try {
const totalConvos = (await SharedLink.countDocuments(query)) || 1;
const totalPages = Math.ceil(totalConvos / pageSize);
const shares = await SharedLink.find(query)
.sort({ updatedAt: -1 })
.skip((pageNumber - 1) * pageSize)
.limit(pageSize)
.lean();
return { sharedLinks: shares, pages: totalPages, pageNumber, pageSize };
} catch (error) {
logger.error('[getShareByPage] Error getting shares', error);
return { message: 'Error getting shares' };
}
},

createSharedLink: async (user, { conversationId, ...shareData }) => {
const share = await SharedLink.findOne({ conversationId }).lean();
if (share) {
return share;
}

try {
const shareId = crypto.randomUUID();
const messages = await getMessages({ conversationId });
const update = { ...shareData, shareId, messages, user };
return await SharedLink.findOneAndUpdate({ conversationId: conversationId, user }, update, {
new: true,
upsert: true,
});
} catch (error) {
logger.error('[saveShareMessage] Error saving conversation', error);
return { message: 'Error saving conversation' };
}
},
updateSharedLink: async (user, { conversationId, ...shareData }) => {
const share = await SharedLink.findOne({ conversationId }).lean();
if (!share) {
return { message: 'Share not found' };
}
// update messages to the latest
const messages = await getMessages({ conversationId });
const update = { ...shareData, messages, user };
return await SharedLink.findOneAndUpdate({ conversationId: conversationId, user }, update, {
new: true,
upsert: false,
});
},

deleteSharedLink: async (user, { shareId }) => {
const share = await SharedLink.findOne({ shareId, user });
if (!share) {
return { message: 'Share not found' };
}
return await SharedLink.findOneAndDelete({ shareId, user });
},
};
38 changes: 38 additions & 0 deletions api/models/schema/shareSchema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const mongoose = require('mongoose');

const shareSchema = mongoose.Schema(
{
conversationId: {
type: String,
required: true,
},
title: {
type: String,
index: true,
},
user: {
type: String,
index: true,
},
messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }],
shareId: {
type: String,
index: true,
},
isPublic: {
type: Boolean,
default: false,
},
isVisible: {
type: Boolean,
default: false,
},
isAnonymous: {
type: Boolean,
default: true,
},
},
{ timestamps: true },
);

module.exports = mongoose.model('SharedLink', shareSchema);
1 change: 1 addition & 0 deletions api/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const startServer = async () => {
app.use('/api/assistants', routes.assistants);
app.use('/api/files', await routes.files.initialize());
app.use('/images/', validateImageRequest, routes.staticRoute);
app.use('/api/share', routes.share);

app.use((req, res) => {
res.status(404).sendFile(path.join(app.locals.paths.dist, 'index.html'));
Expand Down
2 changes: 2 additions & 0 deletions api/server/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const config = require('./config');
const assistants = require('./assistants');
const files = require('./files');
const staticRoute = require('./static');
const share = require('./share');

module.exports = {
search,
Expand All @@ -40,4 +41,5 @@ module.exports = {
assistants,
files,
staticRoute,
share,
};
75 changes: 75 additions & 0 deletions api/server/routes/share.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
const express = require('express');

const {
getSharedMessages,
createSharedLink,
updateSharedLink,
getSharedLinks,
deleteSharedLink,
} = require('~/models/Share');
const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
const router = express.Router();

/**
* Shared messages
* this route does not require authentication
*/
router.get('/:shareId', async (req, res) => {
const share = await getSharedMessages(req.params.shareId);

if (share) {
res.status(200).json(share);
} else {
res.status(404).end();
}
});

/**
* Shared links
*/
router.get('/', requireJwtAuth, async (req, res) => {
let pageNumber = req.query.pageNumber || 1;
pageNumber = parseInt(pageNumber, 10);

if (isNaN(pageNumber) || pageNumber < 1) {
return res.status(400).json({ error: 'Invalid page number' });
}

let pageSize = req.query.pageSize || 25;
pageSize = parseInt(pageSize, 10);

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.post('/', requireJwtAuth, async (req, res) => {
const created = await createSharedLink(req.user.id, req.body);
if (created) {
res.status(200).json(created);
} else {
res.status(404).end();
}
});

router.patch('/', requireJwtAuth, async (req, res) => {
const updated = await updateSharedLink(req.user.id, req.body);
if (updated) {
res.status(200).json(updated);
} else {
res.status(404).end();
}
});

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

module.exports = router;
29 changes: 29 additions & 0 deletions client/src/components/Auth/BlinkAnimation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export const BlinkAnimation = ({
active,
children,
}: {
active: boolean;
children: React.ReactNode;
}) => {
const style = `
@keyframes blink-animation {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}`;

if (!active) {
return <>{children}</>;
}

return (
<>
<style>{style}</style>
<div style={{ animation: 'blink-animation 3s infinite' }}>{children}</div>
</>
);
};
Loading
Loading