feat: Implement server-side comment preloading for improved performance and introduce new comment features including pinned comments, locked threads, and custom emojis.
This commit is contained in:
@@ -57,6 +57,38 @@ class CommentSystem {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for server-side preloaded comments
|
||||||
|
// Check for server-side preloaded comments (Script Tag Method)
|
||||||
|
const dataEl = document.getElementById('initial-comments');
|
||||||
|
if (dataEl) {
|
||||||
|
try {
|
||||||
|
// Decode Base64 for safe template transfer
|
||||||
|
const raw = dataEl.textContent.trim();
|
||||||
|
const json = atob(raw);
|
||||||
|
const comments = JSON.parse(json);
|
||||||
|
|
||||||
|
const subEl = document.getElementById('initial-subscription');
|
||||||
|
// Handle boolean text content
|
||||||
|
const isSubscribed = subEl && (subEl.textContent.trim() === 'true');
|
||||||
|
|
||||||
|
// Consume
|
||||||
|
dataEl.remove();
|
||||||
|
if (subEl) subEl.remove();
|
||||||
|
|
||||||
|
this.render(comments, this.user, isSubscribed);
|
||||||
|
|
||||||
|
if (scrollToId) {
|
||||||
|
this.scrollToComment(scrollToId);
|
||||||
|
} else if (window.location.hash && window.location.hash.startsWith('#c')) {
|
||||||
|
const hashId = window.location.hash.substring(2);
|
||||||
|
this.scrollToComment(hashId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("SSR comments parse error", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Render skeleton (Result: Layout visible immediately)
|
// Render skeleton (Result: Layout visible immediately)
|
||||||
if (!scrollToId) {
|
if (!scrollToId) {
|
||||||
this.render([], this.user, false);
|
this.render([], this.user, false);
|
||||||
|
|||||||
@@ -270,9 +270,37 @@ export default {
|
|||||||
const link = lib.genLink({ user, tag, mime, type: o.fav ? 'favs' : 'f0cks' });
|
const link = lib.genLink({ user, tag, mime, type: o.fav ? 'favs' : 'f0cks' });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
|
||||||
link: link,
|
|
||||||
itemid: item[0].id
|
itemid: item[0].id
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
getComments: async (itemId, sort = 'new') => {
|
||||||
|
if (!itemId) return [];
|
||||||
|
try {
|
||||||
|
const comments = await db`
|
||||||
|
SELECT
|
||||||
|
c.id, c.parent_id, c.content, c.created_at, c.vote_score, c.is_deleted,
|
||||||
|
COALESCE(c.is_pinned, false) as is_pinned,
|
||||||
|
u.user as username, u.id as user_id, uo.avatar,
|
||||||
|
(SELECT count(*) FROM comments r WHERE r.parent_id = c.id) as reply_count
|
||||||
|
FROM comments c
|
||||||
|
JOIN "user" u ON c.user_id = u.id
|
||||||
|
LEFT JOIN user_options uo ON uo.user_id = u.id
|
||||||
|
WHERE c.item_id = ${itemId} AND c.is_deleted = false
|
||||||
|
ORDER BY COALESCE(c.is_pinned, false) DESC, c.created_at ${db.unsafe(sort === 'new' ? 'DESC' : 'ASC')}
|
||||||
|
`;
|
||||||
|
return comments;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[F0CKLIB] Error fetching comments:', e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getSubscriptionStatus: async (userId, itemId) => {
|
||||||
|
if (!userId || !itemId) return false;
|
||||||
|
try {
|
||||||
|
const sub = await db`SELECT 1 FROM comment_subscriptions WHERE user_id = ${userId} AND item_id = ${itemId}`;
|
||||||
|
return sub.length > 0;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,6 +42,26 @@ export default (router, tpl) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Preload comments for instant rendering (if logged in)
|
||||||
|
if (req.session) {
|
||||||
|
data.comments = await f0cklib.getComments(req.params.itemid);
|
||||||
|
// Also need subscription status? comments.js handles subscription toggle separately but initial state?
|
||||||
|
// API returns is_subscribed.
|
||||||
|
// Let's optimize later or just fetch simple comments list.
|
||||||
|
// Subscription status and is_locked/is_admin might be needed for comments.js to FULLY render without API call.
|
||||||
|
// But comments.js fetches API mainly for comments list. It also gets is_admin etc.
|
||||||
|
// If I provide comments list, comments.js skips fetch.
|
||||||
|
// It uses `this.isAdmin` from DOM. `this.isLocked` from DOM.
|
||||||
|
// `isSubscribed`? Not in DOM yet.
|
||||||
|
// I should add `data-is-subscribed` to DOM?
|
||||||
|
const sub = await f0cklib.getSubscriptionStatus(req.session.id, req.params.itemid);
|
||||||
|
data.isSubscribed = sub;
|
||||||
|
data.commentsJSON = Buffer.from(JSON.stringify(data.comments || [])).toString('base64');
|
||||||
|
} else {
|
||||||
|
data.comments = [];
|
||||||
|
data.commentsJSON = Buffer.from('[]').toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
// Inject session into data for the template
|
// Inject session into data for the template
|
||||||
// We clone session to avoid unintended side effects or collisions
|
// We clone session to avoid unintended side effects or collisions
|
||||||
if (req.session) {
|
if (req.session) {
|
||||||
|
|||||||
@@ -29,18 +29,7 @@ export default (router, tpl) => {
|
|||||||
const item = await db`SELECT is_comments_locked FROM items WHERE id = ${itemId}`;
|
const item = await db`SELECT is_comments_locked FROM items WHERE id = ${itemId}`;
|
||||||
const is_locked = item.length > 0 ? item[0].is_comments_locked : false;
|
const is_locked = item.length > 0 ? item[0].is_comments_locked : false;
|
||||||
|
|
||||||
const comments = await db`
|
const comments = await f0cklib.getComments(itemId, sort);
|
||||||
SELECT
|
|
||||||
c.id, c.parent_id, c.content, c.created_at, c.vote_score, c.is_deleted,
|
|
||||||
COALESCE(c.is_pinned, false) as is_pinned,
|
|
||||||
u.user as username, u.id as user_id, uo.avatar,
|
|
||||||
(SELECT count(*) FROM comments r WHERE r.parent_id = c.id) as reply_count
|
|
||||||
FROM comments c
|
|
||||||
JOIN "user" u ON c.user_id = u.id
|
|
||||||
LEFT JOIN user_options uo ON uo.user_id = u.id
|
|
||||||
WHERE c.item_id = ${itemId} AND c.is_deleted = false
|
|
||||||
ORDER BY COALESCE(c.is_pinned, false) DESC, c.created_at ${db.unsafe(sort === 'new' ? 'DESC' : 'ASC')}
|
|
||||||
`;
|
|
||||||
|
|
||||||
let is_subscribed = false;
|
let is_subscribed = false;
|
||||||
if (req.session) {
|
if (req.session) {
|
||||||
|
|||||||
@@ -105,6 +105,16 @@ export default (router, tpl) => {
|
|||||||
|
|
||||||
if (mode === 'item') {
|
if (mode === 'item') {
|
||||||
data.hidePagination = true;
|
data.hidePagination = true;
|
||||||
|
if (req.session) {
|
||||||
|
data.comments = await f0cklib.getComments(req.params.itemid);
|
||||||
|
const sub = await f0cklib.getSubscriptionStatus(req.session.id, req.params.itemid);
|
||||||
|
data.isSubscribed = sub;
|
||||||
|
data.commentsJSON = Buffer.from(JSON.stringify(data.comments || [])).toString('base64');
|
||||||
|
} else {
|
||||||
|
data.comments = [];
|
||||||
|
data.isSubscribed = false;
|
||||||
|
data.commentsJSON = Buffer.from('[]').toString('base64');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.reply({ body: tpl.render(mode, data, req) });
|
return res.reply({ body: tpl.render(mode, data, req) });
|
||||||
|
|||||||
@@ -126,4 +126,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="comments-container" data-item-id="{{ item.id }}" @if(session)data-user="{{ session.user }}"
|
<div id="comments-container" data-item-id="{{ item.id }}" @if(session)data-user="{{ session.user }}"
|
||||||
@if(session.admin)data-is-admin="true" @endif @endif @if(item.is_comments_locked)data-is-locked="true" @endif></div>
|
@if(session.admin)data-is-admin="true" @endif @endif @if(item.is_comments_locked)data-is-locked="true" @endif>
|
||||||
|
<div class="comments-header">
|
||||||
|
<h3>Comments @if(item.is_comments_locked)🔒@endif</h3>
|
||||||
|
<div class="comments-controls">
|
||||||
|
<span>Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if(session && session.user && !item.is_comments_locked)
|
||||||
|
<div class="comment-input main-input">
|
||||||
|
<textarea placeholder="Loading comment section..." disabled></textarea>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<script id="initial-comments" type="application/json">{{ commentsJSON }}</script>
|
||||||
|
<script id="initial-subscription" type="application/json">{{ isSubscribed }}</script>
|
||||||
Reference in New Issue
Block a user