feat(chat): older-history pagination + full-thread server-side search
Older messages (beyond the newest 500) now load as you scroll to the top — the thread endpoint takes a ?before=<created_at> cursor, the client prepends the older page and preserves scroll position (renderThread keepScroll). _hasMoreOlder stops paging when a short page returns. Search now covers the ENTIRE thread, not just the loaded window: new /api/messages/search (DM + group, LIKE with escaped wildcards) returns all matching message ids; the client debounces the query, and jumping to a hit older than the loaded window pages history back (ensureLoadedBack) until the match is in view, then highlights + flashes it. Cap raised to 500. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+27
-4
@@ -761,12 +761,13 @@ route('GET', '/api/messages/thread', async (req, res) => {
|
||||
if (!u) return json(res, 401, { error: 'unauthorized' });
|
||||
const q = new URLSearchParams(req.url.split('?')[1] || '');
|
||||
const peek = !!q.get('peek'); // prefetch only — do NOT mark the conversation read
|
||||
const before = parseInt(q.get('before') || '', 10) || null; // pagination cursor: fetch messages OLDER than this created_at
|
||||
const names = namesFor(u.team_id);
|
||||
const group = q.get('group');
|
||||
if (group) {
|
||||
if (!R.conversations.isMember(group, u.id)) return json(res, 403, { error: 'not a member of this group' });
|
||||
const rows = R.messages.threadByConversation(group);
|
||||
if (!peek) {
|
||||
const rows = R.messages.threadByConversation(group, 500, before);
|
||||
if (!peek && !before) {
|
||||
R.conversations.markRead(group, u.id);
|
||||
const evt = { type: 'group-read', group, by: u.id, byName: names[u.id] || u.email, at: now() };
|
||||
for (const mid of R.conversations.members(group)) { if (mid !== u.id) { try { CHAT.pushToUser(mid, evt); } catch (_) {} } }
|
||||
@@ -782,12 +783,34 @@ route('GET', '/api/messages/thread', async (req, res) => {
|
||||
const other = R.users.resolve(q.get('with')); // follow a merge redirect so a stale peer id still loads the thread
|
||||
if (!other) return json(res, 400, { error: 'with or group required' });
|
||||
if (!R.users.inTenant(other, u.team_id)) return json(res, 404, { error: 'no such contact' });
|
||||
const rows = R.messages.thread(u.team_id, u.id, other);
|
||||
if (!peek) { R.messages.markRead(u.team_id, u.id, other); try { CHAT.pushToUser(other, { type: 'chat-read', by: u.id }); } catch (_) {} }
|
||||
const rows = R.messages.thread(u.team_id, u.id, other, 500, before);
|
||||
if (!peek && !before) { R.messages.markRead(u.team_id, u.id, other); try { CHAT.pushToUser(other, { type: 'chat-read', by: u.id }); } catch (_) {} }
|
||||
const rxBy = groupReactions(R.reactions.forPair(u.team_id, u.id, other), u.id, names);
|
||||
return json(res, 200, rows.map((m) => { const d = buildMsgDTO(m, names, u.id); d.reactions = dtoReactions(rxBy, m.id); return d; }));
|
||||
});
|
||||
|
||||
// Search the ENTIRE thread (not just the loaded window). Returns matching message ids + timestamps,
|
||||
// oldest-first, so the client can jump to any hit and lazy-load the window around it.
|
||||
route('GET', '/api/messages/search', async (req, res) => {
|
||||
const u = currentUser(req);
|
||||
if (!u) return json(res, 401, { error: 'unauthorized' });
|
||||
const q = new URLSearchParams(req.url.split('?')[1] || '');
|
||||
const term = String(q.get('q') || '').trim();
|
||||
if (term.length < 1) return json(res, 200, { hits: [] });
|
||||
const like = '%' + term.replace(/[\\%_]/g, '\\$&') + '%'; // escape LIKE wildcards
|
||||
const group = q.get('group');
|
||||
let rows;
|
||||
if (group) {
|
||||
if (!R.conversations.isMember(group, u.id)) return json(res, 403, { error: 'not a member of this group' });
|
||||
rows = R.messages.searchConversation(group, like);
|
||||
} else {
|
||||
const other = R.users.resolve(q.get('with'));
|
||||
if (!other || !R.users.inTenant(other, u.team_id)) return json(res, 404, { error: 'no such contact' });
|
||||
rows = R.messages.searchThread(u.team_id, u.id, other, like);
|
||||
}
|
||||
return json(res, 200, { hits: rows.map((m) => ({ id: m.id, at: m.created_at })) });
|
||||
});
|
||||
|
||||
// Create a group conversation with the given members (creator is always added).
|
||||
route('POST', '/api/groups', async (req, res) => {
|
||||
const u = currentUser(req);
|
||||
|
||||
Reference in New Issue
Block a user