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:
2026-07-07 15:21:14 +05:30
parent 4fd323fff4
commit eb79823fd2
3 changed files with 101 additions and 26 deletions
+14 -5
View File
@@ -207,12 +207,18 @@ const messages = {
// Full 1:1 (DM) thread between two users (both directions). Take the NEWEST `limit` messages
// (inner DESC), then present them oldest-first. The old plain "ASC LIMIT" returned the OLDEST 300
// and silently dropped everything newer once a thread passed 300 — so new messages "disappeared".
thread: (teamId, a, b, limit = 500) =>
thread: (teamId, a, b, limit = 500, before = null) =>
db.prepare(`SELECT * FROM (
SELECT * FROM messages WHERE team_id=? AND conversation_id IS NULL
AND ((sender_id=? AND recipient_id=?) OR (sender_id=? AND recipient_id=?))
AND (? IS NULL OR created_at < ?)
ORDER BY created_at DESC LIMIT ?
) ORDER BY created_at ASC`).all(teamId, a, b, b, a, limit),
) ORDER BY created_at ASC`).all(teamId, a, b, b, a, before, before, limit),
// Full-thread search (ALL messages, not just the loaded window). `like` is the escaped LIKE pattern.
searchThread: (teamId, a, b, like, limit = 300) =>
db.prepare(`SELECT id, created_at, sender_id FROM messages WHERE team_id=? AND conversation_id IS NULL
AND ((sender_id=? AND recipient_id=?) OR (sender_id=? AND recipient_id=?))
AND deleted=0 AND body LIKE ? ESCAPE '\\' ORDER BY created_at ASC LIMIT ?`).all(teamId, a, b, b, a, like, limit),
markRead: (teamId, recipientId, senderId) =>
db.prepare('UPDATE messages SET read_at=? WHERE team_id=? AND conversation_id IS NULL AND recipient_id=? AND sender_id=? AND read_at IS NULL')
.run(now(), teamId, recipientId, senderId),
@@ -221,10 +227,13 @@ const messages = {
db.prepare('SELECT * FROM messages WHERE team_id=? AND conversation_id IS NULL AND (sender_id=? OR recipient_id=?) ORDER BY created_at DESC LIMIT ?')
.all(teamId, userId, userId, limit),
// Group conversation helpers.
threadByConversation: (conversationId, limit = 500) =>
threadByConversation: (conversationId, limit = 500, before = null) =>
db.prepare(`SELECT * FROM (
SELECT * FROM messages WHERE conversation_id=? ORDER BY created_at DESC LIMIT ?
) ORDER BY created_at ASC`).all(conversationId, limit),
SELECT * FROM messages WHERE conversation_id=? AND (? IS NULL OR created_at < ?) ORDER BY created_at DESC LIMIT ?
) ORDER BY created_at ASC`).all(conversationId, before, before, limit),
searchConversation: (conversationId, like, limit = 300) =>
db.prepare(`SELECT id, created_at, sender_id FROM messages WHERE conversation_id=? AND deleted=0
AND body LIKE ? ESCAPE '\\' ORDER BY created_at ASC LIMIT ?`).all(conversationId, like, limit),
lastInConversation: (conversationId) =>
db.prepare('SELECT * FROM messages WHERE conversation_id=? ORDER BY created_at DESC LIMIT 1').get(conversationId),
unreadInConversation: (conversationId, userId, since) =>