fix(status): compute online status from lastSeen timeout in queries
- commit
- cb21e209b88268e5684398abbcc38e73e34eb0c9
- author
- bnema · · 2026-01-18 22:18 UTC
- parent
- 13a549c
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +23 -6
M src/convex/queries.ts +23 -6
| @@ -2,6 +2,23 @@ import { query } from './_generated/server'; | |||
| 2 | 2 | import { authQuery } from './auth'; | |
| 3 | 3 | import { v } from 'convex/values'; | |
| 4 | 4 | ||
| 5 | + | // Timeout threshold for marking users as offline (2 minutes) | |
| 6 | + | const ONLINE_TIMEOUT_MS = 2 * 60 * 1000; | |
| 7 | + | ||
| 8 | + | // Helper to compute effective status based on lastSeen timeout | |
| 9 | + | function computeEffectiveStatus( | |
| 10 | + | status: string, | |
| 11 | + | lastSeen: number | undefined | |
| 12 | + | ): 'online' | 'away' | 'busy' | 'idle' | 'offline' { | |
| 13 | + | if (status === 'offline') return 'offline'; | |
| 14 | + | const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS; | |
| 15 | + | const effectiveLastSeen = lastSeen ?? 0; | |
| 16 | + | if (effectiveLastSeen < timeoutThreshold) { | |
| 17 | + | return 'offline'; | |
| 18 | + | } | |
| 19 | + | return status as 'online' | 'away' | 'busy' | 'idle'; | |
| 20 | + | } | |
| 21 | + | ||
| 5 | 22 | // ============ PUBLIC QUERIES (no auth required) ============ | |
| 6 | 23 | ||
| 7 | 24 | // Public query: Get messages for a chat room (real-time subscription) | |
| @@ -36,7 +53,7 @@ export const getMessagesPublic = query({ | |||
| 36 | 53 | ? { | |
| 37 | 54 | id: sender._id, | |
| 38 | 55 | nickname: sender.nickname, | |
| 39 | - | status: sender.status, | |
| 56 | + | status: computeEffectiveStatus(sender.status, sender.lastSeen), | |
| 40 | 57 | avatarUrl: sender.avatarUrl | |
| 41 | 58 | } | |
| 42 | 59 | : null | |
| @@ -55,11 +72,11 @@ export const getUsersPublic = query({ | |||
| 55 | 72 | handler: async (ctx) => { | |
| 56 | 73 | const users = await ctx.db.query('users').collect(); | |
| 57 | 74 | ||
| 58 | - | // Return safe user data (no passwords) | |
| 75 | + | // Return safe user data (no passwords) with computed status | |
| 59 | 76 | return users.map((user) => ({ | |
| 60 | 77 | id: user._id, | |
| 61 | 78 | nickname: user.nickname, | |
| 62 | - | status: user.status, | |
| 79 | + | status: computeEffectiveStatus(user.status, user.lastSeen), | |
| 63 | 80 | avatarUrl: user.avatarUrl, | |
| 64 | 81 | lastSeen: user.lastSeen | |
| 65 | 82 | })); | |
| @@ -131,7 +148,7 @@ export const getMessages = authQuery({ | |||
| 131 | 148 | ? { | |
| 132 | 149 | id: sender._id, | |
| 133 | 150 | nickname: sender.nickname, | |
| 134 | - | status: sender.status, | |
| 151 | + | status: computeEffectiveStatus(sender.status, sender.lastSeen), | |
| 135 | 152 | avatarUrl: sender.avatarUrl | |
| 136 | 153 | } | |
| 137 | 154 | : null | |
| @@ -150,11 +167,11 @@ export const getUsers = authQuery({ | |||
| 150 | 167 | handler: async (ctx) => { | |
| 151 | 168 | const users = await ctx.db.query('users').collect(); | |
| 152 | 169 | ||
| 153 | - | // Return safe user data (no passwords) | |
| 170 | + | // Return safe user data (no passwords) with computed status | |
| 154 | 171 | return users.map((user) => ({ | |
| 155 | 172 | id: user._id, | |
| 156 | 173 | nickname: user.nickname, | |
| 157 | - | status: user.status, | |
| 174 | + | status: computeEffectiveStatus(user.status, user.lastSeen), | |
| 158 | 175 | avatarUrl: user.avatarUrl, | |
| 159 | 176 | lastSeen: user.lastSeen | |
| 160 | 177 | })); | |
| @@ -2,6 +2,23 @@ import { query } from './_generated/server'; | |||||
| 2 | import { authQuery } from './auth'; | 2 | import { authQuery } from './auth'; | ||
| 3 | import { v } from 'convex/values'; | 3 | import { v } from 'convex/values'; | ||
| 4 | 4 | ||||
| 5 | + | // Timeout threshold for marking users as offline (2 minutes) | |||
| 6 | + | const ONLINE_TIMEOUT_MS = 2 * 60 * 1000; | |||
| 7 | + | ||||
| 8 | + | // Helper to compute effective status based on lastSeen timeout | |||
| 9 | + | function computeEffectiveStatus( | |||
| 10 | + | status: string, | |||
| 11 | + | lastSeen: number | undefined | |||
| 12 | + | ): 'online' | 'away' | 'busy' | 'idle' | 'offline' { | |||
| 13 | + | if (status === 'offline') return 'offline'; | |||
| 14 | + | const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS; | |||
| 15 | + | const effectiveLastSeen = lastSeen ?? 0; | |||
| 16 | + | if (effectiveLastSeen < timeoutThreshold) { | |||
| 17 | + | return 'offline'; | |||
| 18 | + | } | |||
| 19 | + | return status as 'online' | 'away' | 'busy' | 'idle'; | |||
| 20 | + | } | |||
| 21 | + | ||||
| 5 | // ============ PUBLIC QUERIES (no auth required) ============ | 22 | // ============ PUBLIC QUERIES (no auth required) ============ | ||
| 6 | 23 | ||||
| 7 | // Public query: Get messages for a chat room (real-time subscription) | 24 | // Public query: Get messages for a chat room (real-time subscription) | ||
| @@ -36,7 +53,7 @@ export const getMessagesPublic = query({ | |||||
| 36 | ? { | 53 | ? { | ||
| 37 | id: sender._id, | 54 | id: sender._id, | ||
| 38 | nickname: sender.nickname, | 55 | nickname: sender.nickname, | ||
| 39 | - | status: sender.status, | 56 | + | status: computeEffectiveStatus(sender.status, sender.lastSeen), |
| 40 | avatarUrl: sender.avatarUrl | 57 | avatarUrl: sender.avatarUrl | ||
| 41 | } | 58 | } | ||
| 42 | : null | 59 | : null | ||
| @@ -55,11 +72,11 @@ export const getUsersPublic = query({ | |||||
| 55 | handler: async (ctx) => { | 72 | handler: async (ctx) => { | ||
| 56 | const users = await ctx.db.query('users').collect(); | 73 | const users = await ctx.db.query('users').collect(); | ||
| 57 | 74 | ||||
| 58 | - | // Return safe user data (no passwords) | 75 | + | // Return safe user data (no passwords) with computed status |
| 59 | return users.map((user) => ({ | 76 | return users.map((user) => ({ | ||
| 60 | id: user._id, | 77 | id: user._id, | ||
| 61 | nickname: user.nickname, | 78 | nickname: user.nickname, | ||
| 62 | - | status: user.status, | 79 | + | status: computeEffectiveStatus(user.status, user.lastSeen), |
| 63 | avatarUrl: user.avatarUrl, | 80 | avatarUrl: user.avatarUrl, | ||
| 64 | lastSeen: user.lastSeen | 81 | lastSeen: user.lastSeen | ||
| 65 | })); | 82 | })); | ||
| @@ -131,7 +148,7 @@ export const getMessages = authQuery({ | |||||
| 131 | ? { | 148 | ? { | ||
| 132 | id: sender._id, | 149 | id: sender._id, | ||
| 133 | nickname: sender.nickname, | 150 | nickname: sender.nickname, | ||
| 134 | - | status: sender.status, | 151 | + | status: computeEffectiveStatus(sender.status, sender.lastSeen), |
| 135 | avatarUrl: sender.avatarUrl | 152 | avatarUrl: sender.avatarUrl | ||
| 136 | } | 153 | } | ||
| 137 | : null | 154 | : null | ||
| @@ -150,11 +167,11 @@ export const getUsers = authQuery({ | |||||
| 150 | handler: async (ctx) => { | 167 | handler: async (ctx) => { | ||
| 151 | const users = await ctx.db.query('users').collect(); | 168 | const users = await ctx.db.query('users').collect(); | ||
| 152 | 169 | ||||
| 153 | - | // Return safe user data (no passwords) | 170 | + | // Return safe user data (no passwords) with computed status |
| 154 | return users.map((user) => ({ | 171 | return users.map((user) => ({ | ||
| 155 | id: user._id, | 172 | id: user._id, | ||
| 156 | nickname: user.nickname, | 173 | nickname: user.nickname, | ||
| 157 | - | status: user.status, | 174 | + | status: computeEffectiveStatus(user.status, user.lastSeen), |
| 158 | avatarUrl: user.avatarUrl, | 175 | avatarUrl: user.avatarUrl, | ||
| 159 | lastSeen: user.lastSeen | 176 | lastSeen: user.lastSeen | ||
| 160 | })); | 177 | })); | ||