detached 5a6bd11

fix(chat): scroll to bottom on load and auto-save text style

- Use dedicated flag (hasInitialScrolled) instead of scrollTop===0 check
  for reliable initial scroll behavior
- Add debounced effect to save text style changes to database (1.5s debounce)
  when user modifies font family, size, style, or color
commit
5a6bd116f0928976d7b10f74c40235748b25c1c3
author
Kit · · 2026-01-24 05:40 UTC
parent
27eb0fa
signature
SSH UnverifiedSHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6Tk The key is unknown
1 file · +48 -9
M src/lib/components/chat-room.svelte +48 -9
@@ -203,6 +203,7 @@
203203 let hasMoreMessages = $state(true);
204204 let oldestMessageTimestamp = $state<number | null>(null);
205205 let lastMessageCount = $state(0);
206+ let hasInitialScrolled = $state(false);
206207
207208 // ============ DERIVED STATE ============
208209
@@ -291,6 +292,27 @@
291292 }
292293 });
293294
295+ // Save text style to database when it changes (after initial load)
296+ let lastSavedStyleJson = $state<string | null>(null);
297+ $effect(() => {
298+ // Only save if user is logged in and style has been initialized
299+ if (!currentUser || !hasAppliedUserStyle || !browser) return;
300+
301+ const styleJson = JSON.stringify(currentTextStyle);
302+
303+ // Skip if this is the initial load (set lastSavedStyleJson to current)
304+ if (lastSavedStyleJson === null) {
305+ lastSavedStyleJson = styleJson;
306+ return;
307+ }
308+
309+ // Only save if style actually changed
310+ if (styleJson !== lastSavedStyleJson) {
311+ lastSavedStyleJson = styleJson;
312+ debouncedSaveTextStyle(currentTextStyle);
313+ }
314+ });
315+
294316 // Auto-scroll to bottom when new messages arrive
295317 $effect(() => {
296318 if (messages.length > lastMessageCount && messages.length > 0) {
@@ -307,15 +329,13 @@
307329
308330 // Scroll to bottom on initial load (when chatArea becomes available and messages exist)
309331 $effect(() => {
310- if (browser && chatArea && messages.length > 0 && !isInitialLoading) {
311- // Only run once on initial load by checking if we haven't scrolled yet
312- if (chatArea.scrollTop === 0 && chatArea.scrollHeight > chatArea.clientHeight) {
313- tick().then(() => {
314- if (chatArea) {
315- chatArea.scrollTop = chatArea.scrollHeight;
316- }
317- });
318- }
332+ if (browser && chatArea && messages.length > 0 && !isInitialLoading && !hasInitialScrolled) {
333+ hasInitialScrolled = true;
334+ tick().then(() => {
335+ if (chatArea) {
336+ chatArea.scrollTop = chatArea.scrollHeight;
337+ }
338+ });
319339 }
320340 });
321341
@@ -571,6 +591,25 @@
571591 }
572592 }, 1000);
573593
594+ // Debounced function to save text style preferences to the database
595+ const debouncedSaveTextStyle = debounce(async (style: TextStyle) => {
596+ if (!currentUser || !browser) return;
597+ try {
598+ await fetch('/api/user/text-preferences', {
599+ method: 'POST',
600+ headers: { 'Content-Type': 'application/json' },
601+ credentials: 'include',
602+ body: JSON.stringify({
603+ defaultStyle: style,
604+ allowFormatting: true,
605+ maxMessageLength: MAX_MESSAGE_LENGTH
606+ })
607+ });
608+ } catch (error) {
609+ console.error('Failed to save text style preferences:', error);
610+ }
611+ }, 1500);
612+
574613 function setsEqual(a: Set<string>, b: Set<string>) {
575614 if (a.size !== b.size) return false;
576615 for (const value of a) {
@@ -203,6 +203,7 @@
203 let hasMoreMessages = $state(true);203 let hasMoreMessages = $state(true);
204 let oldestMessageTimestamp = $state<number | null>(null);204 let oldestMessageTimestamp = $state<number | null>(null);
205 let lastMessageCount = $state(0);205 let lastMessageCount = $state(0);
206+ let hasInitialScrolled = $state(false);
206 207
207 // ============ DERIVED STATE ============208 // ============ DERIVED STATE ============
208 209
@@ -291,6 +292,27 @@
291 }292 }
292 });293 });
293 294
295+ // Save text style to database when it changes (after initial load)
296+ let lastSavedStyleJson = $state<string | null>(null);
297+ $effect(() => {
298+ // Only save if user is logged in and style has been initialized
299+ if (!currentUser || !hasAppliedUserStyle || !browser) return;
300+
301+ const styleJson = JSON.stringify(currentTextStyle);
302+
303+ // Skip if this is the initial load (set lastSavedStyleJson to current)
304+ if (lastSavedStyleJson === null) {
305+ lastSavedStyleJson = styleJson;
306+ return;
307+ }
308+
309+ // Only save if style actually changed
310+ if (styleJson !== lastSavedStyleJson) {
311+ lastSavedStyleJson = styleJson;
312+ debouncedSaveTextStyle(currentTextStyle);
313+ }
314+ });
315+
294 // Auto-scroll to bottom when new messages arrive316 // Auto-scroll to bottom when new messages arrive
295 $effect(() => {317 $effect(() => {
296 if (messages.length > lastMessageCount && messages.length > 0) {318 if (messages.length > lastMessageCount && messages.length > 0) {
@@ -307,15 +329,13 @@
307 329
308 // Scroll to bottom on initial load (when chatArea becomes available and messages exist)330 // Scroll to bottom on initial load (when chatArea becomes available and messages exist)
309 $effect(() => {331 $effect(() => {
310- if (browser && chatArea && messages.length > 0 && !isInitialLoading) {332+ if (browser && chatArea && messages.length > 0 && !isInitialLoading && !hasInitialScrolled) {
311- // Only run once on initial load by checking if we haven't scrolled yet333+ hasInitialScrolled = true;
312- if (chatArea.scrollTop === 0 && chatArea.scrollHeight > chatArea.clientHeight) {334+ tick().then(() => {
313- tick().then(() => {335+ if (chatArea) {
314- if (chatArea) {336+ chatArea.scrollTop = chatArea.scrollHeight;
315- chatArea.scrollTop = chatArea.scrollHeight;337+ }
316- }338+ });
317- });
318- }
319 }339 }
320 });340 });
321 341
@@ -571,6 +591,25 @@
571 }591 }
572 }, 1000);592 }, 1000);
573 593
594+ // Debounced function to save text style preferences to the database
595+ const debouncedSaveTextStyle = debounce(async (style: TextStyle) => {
596+ if (!currentUser || !browser) return;
597+ try {
598+ await fetch('/api/user/text-preferences', {
599+ method: 'POST',
600+ headers: { 'Content-Type': 'application/json' },
601+ credentials: 'include',
602+ body: JSON.stringify({
603+ defaultStyle: style,
604+ allowFormatting: true,
605+ maxMessageLength: MAX_MESSAGE_LENGTH
606+ })
607+ });
608+ } catch (error) {
609+ console.error('Failed to save text style preferences:', error);
610+ }
611+ }, 1500);
612+
574 function setsEqual(a: Set<string>, b: Set<string>) {613 function setsEqual(a: Set<string>, b: Set<string>) {
575 if (a.size !== b.size) return false;614 if (a.size !== b.size) return false;
576 for (const value of a) {615 for (const value of a) {

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