fix: restore text preferences auth and chat pagination
- commit
- f8c7979995ac31128028de093a4980a0aec03efc
- author
- bnema · · 2026-01-18 20:25 UTC
- parent
- 39c9c53
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +61 -12
M src/lib/components/chat-room.svelte +53 -8
| @@ -91,6 +91,21 @@ | |||
| 91 | 91 | })); | |
| 92 | 92 | }); | |
| 93 | 93 | ||
| 94 | + | // Messages loaded via pagination (older history) | |
| 95 | + | let pagedMessages = $state<Message[]>([]); | |
| 96 | + | ||
| 97 | + | // Merge paginated messages with live subscription data | |
| 98 | + | const allMessages = $derived.by<EnrichedMessage[]>(() => { | |
| 99 | + | const merged = new Map<string, EnrichedMessage>(); | |
| 100 | + | for (const msg of pagedMessages) { | |
| 101 | + | merged.set(msg.id, enrichMessage(msg)); | |
| 102 | + | } | |
| 103 | + | for (const msg of messages) { | |
| 104 | + | merged.set(msg.id, msg); | |
| 105 | + | } | |
| 106 | + | return Array.from(merged.values()).sort((a, b) => a.timestamp - b.timestamp); | |
| 107 | + | }); | |
| 108 | + | ||
| 94 | 109 | // Loading and error states from Convex | |
| 95 | 110 | const isInitialLoading = $derived( | |
| 96 | 111 | defaultRoomQuery.isLoading || usersQuery.isLoading || messagesQuery?.isLoading | |
| @@ -165,11 +180,11 @@ | |||
| 165 | 180 | // Visible messages (limit for non-logged-in users) | |
| 166 | 181 | const visibleMessages = $derived.by<EnrichedMessage[]>(() => { | |
| 167 | 182 | const isLoggedIn = Boolean(currentUser); | |
| 168 | - | return isLoggedIn ? messages : messages.slice(Math.max(0, messages.length - 50)); | |
| 183 | + | return isLoggedIn ? allMessages : allMessages.slice(Math.max(0, allMessages.length - 50)); | |
| 169 | 184 | }); | |
| 170 | 185 | ||
| 171 | 186 | // Show registration prompt for non-logged-in users | |
| 172 | - | const showRegistrationPrompt = $derived(!currentUser && messages.length > 50); | |
| 187 | + | const showRegistrationPrompt = $derived(!currentUser && allMessages.length > 50); | |
| 173 | 188 | ||
| 174 | 189 | // Character counter | |
| 175 | 190 | const CHAR_WARNING_THRESHOLD = Math.floor(MAX_MESSAGE_LENGTH * 0.8); | |
| @@ -206,13 +221,40 @@ | |||
| 206 | 221 | ||
| 207 | 222 | // Update oldest timestamp for pagination | |
| 208 | 223 | $effect(() => { | |
| 209 | - | if (messages.length > 0) { | |
| 210 | - | oldestMessageTimestamp = Math.min(...messages.map((m) => m.timestamp)); | |
| 224 | + | if (allMessages.length > 0) { | |
| 225 | + | oldestMessageTimestamp = Math.min(...allMessages.map((m) => m.timestamp)); | |
| 211 | 226 | } | |
| 212 | 227 | }); | |
| 213 | 228 | ||
| 214 | 229 | // ============ HELPERS ============ | |
| 215 | 230 | ||
| 231 | + | function resolveUser(senderId: string): SafeUser { | |
| 232 | + | const knownUser = onlineUsers.find((user) => user.id === senderId); | |
| 233 | + | if (knownUser) return knownUser; | |
| 234 | + | return { | |
| 235 | + | id: senderId, | |
| 236 | + | nickname: 'Unknown User', | |
| 237 | + | status: 'offline', | |
| 238 | + | avatarUrl: null, | |
| 239 | + | lastSeen: null | |
| 240 | + | }; | |
| 241 | + | } | |
| 242 | + | ||
| 243 | + | function enrichMessage(message: Message): EnrichedMessage { | |
| 244 | + | return { | |
| 245 | + | ...message, | |
| 246 | + | user: resolveUser(message.senderId) | |
| 247 | + | }; | |
| 248 | + | } | |
| 249 | + | ||
| 250 | + | function mergePagedMessages(incoming: Message[]) { | |
| 251 | + | if (incoming.length === 0) return; | |
| 252 | + | const existingIds = new Set(pagedMessages.map((message) => message.id)); | |
| 253 | + | const newMessages = incoming.filter((message) => !existingIds.has(message.id)); | |
| 254 | + | if (newMessages.length === 0) return; | |
| 255 | + | pagedMessages = [...pagedMessages, ...newMessages]; | |
| 256 | + | } | |
| 257 | + | ||
| 216 | 258 | const debouncedSaveWindowState = debounce(saveWindowState, 300); | |
| 217 | 259 | ||
| 218 | 260 | function updateCooldownProgress() { | |
| @@ -365,16 +407,19 @@ | |||
| 365 | 407 | if (data.messages.length === 0) { | |
| 366 | 408 | hasMoreMessages = false; | |
| 367 | 409 | } else { | |
| 368 | - | // Messages will be updated via Convex subscription | |
| 369 | - | // Just update pagination state | |
| 410 | + | const fetchedMessages = data.messages as Message[]; | |
| 411 | + | mergePagedMessages(fetchedMessages); | |
| 412 | + | ||
| 370 | 413 | requestAnimationFrame(() => { | |
| 371 | 414 | const newScrollHeight = chatArea.scrollHeight; | |
| 372 | 415 | const heightDifference = newScrollHeight - scrollHeight; | |
| 373 | 416 | chatArea.scrollTop = currentScrollTop + heightDifference; | |
| 374 | 417 | }); | |
| 375 | 418 | ||
| 376 | - | const newOldest = Math.min(...data.messages.map((m: Message) => m.timestamp)); | |
| 377 | - | oldestMessageTimestamp = newOldest; | |
| 419 | + | const newOldest = Math.min(...fetchedMessages.map((m) => m.timestamp)); | |
| 420 | + | oldestMessageTimestamp = oldestMessageTimestamp | |
| 421 | + | ? Math.min(oldestMessageTimestamp, newOldest) | |
| 422 | + | : newOldest; | |
| 378 | 423 | hasMoreMessages = data.hasMore; | |
| 379 | 424 | } | |
| 380 | 425 | } | |
| @@ -91,6 +91,21 @@ | |||||
| 91 | })); | 91 | })); | ||
| 92 | }); | 92 | }); | ||
| 93 | 93 | ||||
| 94 | + | // Messages loaded via pagination (older history) | |||
| 95 | + | let pagedMessages = $state<Message[]>([]); | |||
| 96 | + | ||||
| 97 | + | // Merge paginated messages with live subscription data | |||
| 98 | + | const allMessages = $derived.by<EnrichedMessage[]>(() => { | |||
| 99 | + | const merged = new Map<string, EnrichedMessage>(); | |||
| 100 | + | for (const msg of pagedMessages) { | |||
| 101 | + | merged.set(msg.id, enrichMessage(msg)); | |||
| 102 | + | } | |||
| 103 | + | for (const msg of messages) { | |||
| 104 | + | merged.set(msg.id, msg); | |||
| 105 | + | } | |||
| 106 | + | return Array.from(merged.values()).sort((a, b) => a.timestamp - b.timestamp); | |||
| 107 | + | }); | |||
| 108 | + | ||||
| 94 | // Loading and error states from Convex | 109 | // Loading and error states from Convex | ||
| 95 | const isInitialLoading = $derived( | 110 | const isInitialLoading = $derived( | ||
| 96 | defaultRoomQuery.isLoading || usersQuery.isLoading || messagesQuery?.isLoading | 111 | defaultRoomQuery.isLoading || usersQuery.isLoading || messagesQuery?.isLoading | ||
| @@ -165,11 +180,11 @@ | |||||
| 165 | // Visible messages (limit for non-logged-in users) | 180 | // Visible messages (limit for non-logged-in users) | ||
| 166 | const visibleMessages = $derived.by<EnrichedMessage[]>(() => { | 181 | const visibleMessages = $derived.by<EnrichedMessage[]>(() => { | ||
| 167 | const isLoggedIn = Boolean(currentUser); | 182 | const isLoggedIn = Boolean(currentUser); | ||
| 168 | - | return isLoggedIn ? messages : messages.slice(Math.max(0, messages.length - 50)); | 183 | + | return isLoggedIn ? allMessages : allMessages.slice(Math.max(0, allMessages.length - 50)); |
| 169 | }); | 184 | }); | ||
| 170 | 185 | ||||
| 171 | // Show registration prompt for non-logged-in users | 186 | // Show registration prompt for non-logged-in users | ||
| 172 | - | const showRegistrationPrompt = $derived(!currentUser && messages.length > 50); | 187 | + | const showRegistrationPrompt = $derived(!currentUser && allMessages.length > 50); |
| 173 | 188 | ||||
| 174 | // Character counter | 189 | // Character counter | ||
| 175 | const CHAR_WARNING_THRESHOLD = Math.floor(MAX_MESSAGE_LENGTH * 0.8); | 190 | const CHAR_WARNING_THRESHOLD = Math.floor(MAX_MESSAGE_LENGTH * 0.8); | ||
| @@ -206,13 +221,40 @@ | |||||
| 206 | 221 | ||||
| 207 | // Update oldest timestamp for pagination | 222 | // Update oldest timestamp for pagination | ||
| 208 | $effect(() => { | 223 | $effect(() => { | ||
| 209 | - | if (messages.length > 0) { | 224 | + | if (allMessages.length > 0) { |
| 210 | - | oldestMessageTimestamp = Math.min(...messages.map((m) => m.timestamp)); | 225 | + | oldestMessageTimestamp = Math.min(...allMessages.map((m) => m.timestamp)); |
| 211 | } | 226 | } | ||
| 212 | }); | 227 | }); | ||
| 213 | 228 | ||||
| 214 | // ============ HELPERS ============ | 229 | // ============ HELPERS ============ | ||
| 215 | 230 | ||||
| 231 | + | function resolveUser(senderId: string): SafeUser { | |||
| 232 | + | const knownUser = onlineUsers.find((user) => user.id === senderId); | |||
| 233 | + | if (knownUser) return knownUser; | |||
| 234 | + | return { | |||
| 235 | + | id: senderId, | |||
| 236 | + | nickname: 'Unknown User', | |||
| 237 | + | status: 'offline', | |||
| 238 | + | avatarUrl: null, | |||
| 239 | + | lastSeen: null | |||
| 240 | + | }; | |||
| 241 | + | } | |||
| 242 | + | ||||
| 243 | + | function enrichMessage(message: Message): EnrichedMessage { | |||
| 244 | + | return { | |||
| 245 | + | ...message, | |||
| 246 | + | user: resolveUser(message.senderId) | |||
| 247 | + | }; | |||
| 248 | + | } | |||
| 249 | + | ||||
| 250 | + | function mergePagedMessages(incoming: Message[]) { | |||
| 251 | + | if (incoming.length === 0) return; | |||
| 252 | + | const existingIds = new Set(pagedMessages.map((message) => message.id)); | |||
| 253 | + | const newMessages = incoming.filter((message) => !existingIds.has(message.id)); | |||
| 254 | + | if (newMessages.length === 0) return; | |||
| 255 | + | pagedMessages = [...pagedMessages, ...newMessages]; | |||
| 256 | + | } | |||
| 257 | + | ||||
| 216 | const debouncedSaveWindowState = debounce(saveWindowState, 300); | 258 | const debouncedSaveWindowState = debounce(saveWindowState, 300); | ||
| 217 | 259 | ||||
| 218 | function updateCooldownProgress() { | 260 | function updateCooldownProgress() { | ||
| @@ -365,16 +407,19 @@ | |||||
| 365 | if (data.messages.length === 0) { | 407 | if (data.messages.length === 0) { | ||
| 366 | hasMoreMessages = false; | 408 | hasMoreMessages = false; | ||
| 367 | } else { | 409 | } else { | ||
| 368 | - | // Messages will be updated via Convex subscription | 410 | + | const fetchedMessages = data.messages as Message[]; |
| 369 | - | // Just update pagination state | 411 | + | mergePagedMessages(fetchedMessages); |
| 412 | + | ||||
| 370 | requestAnimationFrame(() => { | 413 | requestAnimationFrame(() => { | ||
| 371 | const newScrollHeight = chatArea.scrollHeight; | 414 | const newScrollHeight = chatArea.scrollHeight; | ||
| 372 | const heightDifference = newScrollHeight - scrollHeight; | 415 | const heightDifference = newScrollHeight - scrollHeight; | ||
| 373 | chatArea.scrollTop = currentScrollTop + heightDifference; | 416 | chatArea.scrollTop = currentScrollTop + heightDifference; | ||
| 374 | }); | 417 | }); | ||
| 375 | 418 | ||||
| 376 | - | const newOldest = Math.min(...data.messages.map((m: Message) => m.timestamp)); | 419 | + | const newOldest = Math.min(...fetchedMessages.map((m) => m.timestamp)); |
| 377 | - | oldestMessageTimestamp = newOldest; | 420 | + | oldestMessageTimestamp = oldestMessageTimestamp |
| 421 | + | ? Math.min(oldestMessageTimestamp, newOldest) | |||
| 422 | + | : newOldest; | |||
| 378 | hasMoreMessages = data.hasMore; | 423 | hasMoreMessages = data.hasMore; | ||
| 379 | } | 424 | } | ||
| 380 | } | 425 | } | ||
M src/routes/api/user/text-preferences/+server.ts +8 -4
| @@ -4,11 +4,15 @@ import convex from '$lib/db/convex.server'; | |||
| 4 | 4 | import { DEFAULT_USER_PREFERENCES, type UserTextPreferences } from '$lib/types/text-formatting'; | |
| 5 | 5 | import { validateSessionToken } from '$lib/api/session.server'; | |
| 6 | 6 | ||
| 7 | + | const isKitHttpError = (err: unknown): err is { status: number; body: unknown } => { | |
| 8 | + | return typeof err === 'object' && err !== null && 'status' in err && 'body' in err; | |
| 9 | + | }; | |
| 10 | + | ||
| 7 | 11 | // GET - Fetch current user's text preferences | |
| 8 | 12 | export const GET: RequestHandler = async ({ cookies }) => { | |
| 9 | 13 | try { | |
| 10 | 14 | // Get session token from cookie | |
| 11 | - | const sessionToken = cookies.get('pdraim_session'); | |
| 15 | + | const sessionToken = cookies.get('session'); | |
| 12 | 16 | if (!sessionToken) { | |
| 13 | 17 | throw error(401, 'No session found'); | |
| 14 | 18 | } | |
| @@ -44,7 +48,7 @@ export const GET: RequestHandler = async ({ cookies }) => { | |||
| 44 | 48 | }); | |
| 45 | 49 | } catch (err) { | |
| 46 | 50 | console.error('Failed to fetch text preferences:', err); | |
| 47 | - | if (err instanceof Response) { | |
| 51 | + | if (err instanceof Response || isKitHttpError(err)) { | |
| 48 | 52 | throw err; | |
| 49 | 53 | } | |
| 50 | 54 | throw error(500, 'Failed to fetch preferences'); | |
| @@ -55,7 +59,7 @@ export const GET: RequestHandler = async ({ cookies }) => { | |||
| 55 | 59 | export const POST: RequestHandler = async ({ cookies, request }) => { | |
| 56 | 60 | try { | |
| 57 | 61 | // Get session token from cookie | |
| 58 | - | const sessionToken = cookies.get('pdraim_session'); | |
| 62 | + | const sessionToken = cookies.get('session'); | |
| 59 | 63 | if (!sessionToken) { | |
| 60 | 64 | throw error(401, 'No session found'); | |
| 61 | 65 | } | |
| @@ -94,7 +98,7 @@ export const POST: RequestHandler = async ({ cookies, request }) => { | |||
| 94 | 98 | }); | |
| 95 | 99 | } catch (err) { | |
| 96 | 100 | console.error('Failed to save text preferences:', err); | |
| 97 | - | if (err instanceof Response) { | |
| 101 | + | if (err instanceof Response || isKitHttpError(err)) { | |
| 98 | 102 | throw err; | |
| 99 | 103 | } | |
| 100 | 104 | throw error(500, 'Failed to save preferences'); | |
| @@ -4,11 +4,15 @@ import convex from '$lib/db/convex.server'; | |||||
| 4 | import { DEFAULT_USER_PREFERENCES, type UserTextPreferences } from '$lib/types/text-formatting'; | 4 | import { DEFAULT_USER_PREFERENCES, type UserTextPreferences } from '$lib/types/text-formatting'; | ||
| 5 | import { validateSessionToken } from '$lib/api/session.server'; | 5 | import { validateSessionToken } from '$lib/api/session.server'; | ||
| 6 | 6 | ||||
| 7 | + | const isKitHttpError = (err: unknown): err is { status: number; body: unknown } => { | |||
| 8 | + | return typeof err === 'object' && err !== null && 'status' in err && 'body' in err; | |||
| 9 | + | }; | |||
| 10 | + | ||||
| 7 | // GET - Fetch current user's text preferences | 11 | // GET - Fetch current user's text preferences | ||
| 8 | export const GET: RequestHandler = async ({ cookies }) => { | 12 | export const GET: RequestHandler = async ({ cookies }) => { | ||
| 9 | try { | 13 | try { | ||
| 10 | // Get session token from cookie | 14 | // Get session token from cookie | ||
| 11 | - | const sessionToken = cookies.get('pdraim_session'); | 15 | + | const sessionToken = cookies.get('session'); |
| 12 | if (!sessionToken) { | 16 | if (!sessionToken) { | ||
| 13 | throw error(401, 'No session found'); | 17 | throw error(401, 'No session found'); | ||
| 14 | } | 18 | } | ||
| @@ -44,7 +48,7 @@ export const GET: RequestHandler = async ({ cookies }) => { | |||||
| 44 | }); | 48 | }); | ||
| 45 | } catch (err) { | 49 | } catch (err) { | ||
| 46 | console.error('Failed to fetch text preferences:', err); | 50 | console.error('Failed to fetch text preferences:', err); | ||
| 47 | - | if (err instanceof Response) { | 51 | + | if (err instanceof Response || isKitHttpError(err)) { |
| 48 | throw err; | 52 | throw err; | ||
| 49 | } | 53 | } | ||
| 50 | throw error(500, 'Failed to fetch preferences'); | 54 | throw error(500, 'Failed to fetch preferences'); | ||
| @@ -55,7 +59,7 @@ export const GET: RequestHandler = async ({ cookies }) => { | |||||
| 55 | export const POST: RequestHandler = async ({ cookies, request }) => { | 59 | export const POST: RequestHandler = async ({ cookies, request }) => { | ||
| 56 | try { | 60 | try { | ||
| 57 | // Get session token from cookie | 61 | // Get session token from cookie | ||
| 58 | - | const sessionToken = cookies.get('pdraim_session'); | 62 | + | const sessionToken = cookies.get('session'); |
| 59 | if (!sessionToken) { | 63 | if (!sessionToken) { | ||
| 60 | throw error(401, 'No session found'); | 64 | throw error(401, 'No session found'); | ||
| 61 | } | 65 | } | ||
| @@ -94,7 +98,7 @@ export const POST: RequestHandler = async ({ cookies, request }) => { | |||||
| 94 | }); | 98 | }); | ||
| 95 | } catch (err) { | 99 | } catch (err) { | ||
| 96 | console.error('Failed to save text preferences:', err); | 100 | console.error('Failed to save text preferences:', err); | ||
| 97 | - | if (err instanceof Response) { | 101 | + | if (err instanceof Response || isKitHttpError(err)) { |
| 98 | throw err; | 102 | throw err; | ||
| 99 | } | 103 | } | ||
| 100 | throw error(500, 'Failed to save preferences'); | 104 | throw error(500, 'Failed to save preferences'); | ||