detached f8c7979

fix: restore text preferences auth and chat pagination

commit
f8c7979995ac31128028de093a4980a0aec03efc
author
bnema · · 2026-01-18 20:25 UTC
parent
39c9c53
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +61 -12
M src/lib/components/chat-room.svelte +53 -8
@@ -91,6 +91,21 @@
9191 }));
9292 });
9393
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+
94109 // Loading and error states from Convex
95110 const isInitialLoading = $derived(
96111 defaultRoomQuery.isLoading || usersQuery.isLoading || messagesQuery?.isLoading
@@ -165,11 +180,11 @@
165180 // Visible messages (limit for non-logged-in users)
166181 const visibleMessages = $derived.by<EnrichedMessage[]>(() => {
167182 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));
169184 });
170185
171186 // Show registration prompt for non-logged-in users
172- const showRegistrationPrompt = $derived(!currentUser && messages.length > 50);
187+ const showRegistrationPrompt = $derived(!currentUser && allMessages.length > 50);
173188
174189 // Character counter
175190 const CHAR_WARNING_THRESHOLD = Math.floor(MAX_MESSAGE_LENGTH * 0.8);
@@ -206,13 +221,40 @@
206221
207222 // Update oldest timestamp for pagination
208223 $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));
211226 }
212227 });
213228
214229 // ============ HELPERS ============
215230
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+
216258 const debouncedSaveWindowState = debounce(saveWindowState, 300);
217259
218260 function updateCooldownProgress() {
@@ -365,16 +407,19 @@
365407 if (data.messages.length === 0) {
366408 hasMoreMessages = false;
367409 } 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+
370413 requestAnimationFrame(() => {
371414 const newScrollHeight = chatArea.scrollHeight;
372415 const heightDifference = newScrollHeight - scrollHeight;
373416 chatArea.scrollTop = currentScrollTop + heightDifference;
374417 });
375418
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;
378423 hasMoreMessages = data.hasMore;
379424 }
380425 }
@@ -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 Convex109 // Loading and error states from Convex
95 const isInitialLoading = $derived(110 const isInitialLoading = $derived(
96 defaultRoomQuery.isLoading || usersQuery.isLoading || messagesQuery?.isLoading111 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 users186 // 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 counter189 // 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 pagination222 // 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 subscription410+ const fetchedMessages = data.messages as Message[];
369- // Just update pagination state411+ 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';
44 import { DEFAULT_USER_PREFERENCES, type UserTextPreferences } from '$lib/types/text-formatting';
55 import { validateSessionToken } from '$lib/api/session.server';
66
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+
711 // GET - Fetch current user's text preferences
812 export const GET: RequestHandler = async ({ cookies }) => {
913 try {
1014 // Get session token from cookie
11- const sessionToken = cookies.get('pdraim_session');
15+ const sessionToken = cookies.get('session');
1216 if (!sessionToken) {
1317 throw error(401, 'No session found');
1418 }
@@ -44,7 +48,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
4448 });
4549 } catch (err) {
4650 console.error('Failed to fetch text preferences:', err);
47- if (err instanceof Response) {
51+ if (err instanceof Response || isKitHttpError(err)) {
4852 throw err;
4953 }
5054 throw error(500, 'Failed to fetch preferences');
@@ -55,7 +59,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
5559 export const POST: RequestHandler = async ({ cookies, request }) => {
5660 try {
5761 // Get session token from cookie
58- const sessionToken = cookies.get('pdraim_session');
62+ const sessionToken = cookies.get('session');
5963 if (!sessionToken) {
6064 throw error(401, 'No session found');
6165 }
@@ -94,7 +98,7 @@ export const POST: RequestHandler = async ({ cookies, request }) => {
9498 });
9599 } catch (err) {
96100 console.error('Failed to save text preferences:', err);
97- if (err instanceof Response) {
101+ if (err instanceof Response || isKitHttpError(err)) {
98102 throw err;
99103 }
100104 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 preferences11 // 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 cookie14 // 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 cookie61 // 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');

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