detached d17e56d

fix(aim): count only incoming unread IMs with an index range scan

commit
d17e56d97fb0162770ab5be460bfd4de7e004e19
author
Kit · · 2026-09-29 06:01 UTC
parent
a826f44
signature
SSH UnverifiedSHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6Tk The key is unknown
1 file · +13 -10
M src/convex/aim.ts +13 -10
@@ -121,19 +121,22 @@ export const getMyDirectRooms = authQuery({
121121 const otherId = (room.memberIds ?? []).find((id) => id !== me);
122122 const other = otherId ? await ctx.db.get(otherId) : null;
123123 const lastRead = readMap.get(room._id) ?? 0;
124- // Walk newest-first until the read cursor, scanning at most MAX_UNREAD_COUNT
125- // messages; a full scan still unread saturates the badge ("99+").
126- const recent = await ctx.db
124+ const lastMessage = await ctx.db
127125 .query('messages')
128126 .withIndex('by_chatRoom', (q) => q.eq('chatRoomId', room._id))
129127 .order('desc')
130- .take(MAX_UNREAD_COUNT);
131- const lastMessage = recent[0] ?? null;
132- const unread = recent.filter((m) => m.timestamp > lastRead);
133- const unreadCount =
134- unread.length === MAX_UNREAD_COUNT
135- ? MAX_UNREAD_COUNT
136- : unread.filter((m) => m.senderId !== me).length;
128+ .first();
129+ // Incoming messages after the read cursor, capped for the "99+" badge. Every index
130+ // ends with _creationTime, so the range scan stops at the cursor.
131+ const unreadCount = (
132+ await ctx.db
133+ .query('messages')
134+ .withIndex('by_chatRoom', (q) =>
135+ q.eq('chatRoomId', room._id).gt('_creationTime', lastRead)
136+ )
137+ .filter((q) => q.neq(q.field('senderId'), me))
138+ .take(MAX_UNREAD_COUNT)
139+ ).length;
137140 return {
138141 id: room._id,
139142 other: other ? toPublicUser(other) : null,
@@ -121,19 +121,22 @@ export const getMyDirectRooms = authQuery({
121 const otherId = (room.memberIds ?? []).find((id) => id !== me);121 const otherId = (room.memberIds ?? []).find((id) => id !== me);
122 const other = otherId ? await ctx.db.get(otherId) : null;122 const other = otherId ? await ctx.db.get(otherId) : null;
123 const lastRead = readMap.get(room._id) ?? 0;123 const lastRead = readMap.get(room._id) ?? 0;
124- // Walk newest-first until the read cursor, scanning at most MAX_UNREAD_COUNT124+ const lastMessage = await ctx.db
125- // messages; a full scan still unread saturates the badge ("99+").
126- const recent = await ctx.db
127 .query('messages')125 .query('messages')
128 .withIndex('by_chatRoom', (q) => q.eq('chatRoomId', room._id))126 .withIndex('by_chatRoom', (q) => q.eq('chatRoomId', room._id))
129 .order('desc')127 .order('desc')
130- .take(MAX_UNREAD_COUNT);128+ .first();
131- const lastMessage = recent[0] ?? null;129+ // Incoming messages after the read cursor, capped for the "99+" badge. Every index
132- const unread = recent.filter((m) => m.timestamp > lastRead);130+ // ends with _creationTime, so the range scan stops at the cursor.
133- const unreadCount =131+ const unreadCount = (
134- unread.length === MAX_UNREAD_COUNT132+ await ctx.db
135- ? MAX_UNREAD_COUNT133+ .query('messages')
136- : unread.filter((m) => m.senderId !== me).length;134+ .withIndex('by_chatRoom', (q) =>
135+ q.eq('chatRoomId', room._id).gt('_creationTime', lastRead)
136+ )
137+ .filter((q) => q.neq(q.field('senderId'), me))
138+ .take(MAX_UNREAD_COUNT)
139+ ).length;
137 return {140 return {
138 id: room._id,141 id: room._id,
139 other: other ? toPublicUser(other) : null,142 other: other ? toPublicUser(other) : null,

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close