detached 6251ded

feat: add loading state for Send buttonm

commit
6251ded5f765f43761fad26a403d1a1ef1930cd9
author
Kit · · 2025-02-13 07:01 UTC
parent
64c03e0
signature
SSH UnverifiedSHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6Tk The key is unknown
4 files · +105 -12
M src/lib/components/chat-room.svelte +18 -7
@@ -6,6 +6,7 @@ import { browser } from '$app/environment';
66 import { draggable } from '$lib/actions/draggable';
77 import { resizable } from '$lib/actions/resizable';
88 import { maximizable } from '$lib/actions/maximizable';
9+import LoadingButton from './ui/button-loading.svelte';
910
1011 // Initialize with default values for SSR
1112 let windowWidth = $state(400);
@@ -131,16 +132,20 @@ onMount(() => {
131132 return () => window.removeEventListener('resize', handleResize);
132133 });
133134
135+// New reactive state to track when a message is being sent
136+let isSendingMessage = $state(false);
137+
134138 async function handleSubmit() {
135139 if (!currentMessage.trim()) return;
136-
140+ isSendingMessage = true;
137141 try {
138142 await chatState.sendMessage(currentMessage);
139143 currentMessage = '';
140144 } catch (error) {
141145 console.error('Failed to send message:', error);
142- // You might want to show this error to the user in a more friendly way
143146 alert('Failed to send message. Please try again.');
147+ } finally {
148+ isSendingMessage = false;
144149 }
145150 }
146151
@@ -262,7 +267,12 @@ function handleMaximizeEvent(event: CustomEvent<{
262267 placeholder="Type a message..."
263268 disabled={!currentUser}
264269 />
265- <button onclick={handleSubmit} disabled={!currentUser}>Send</button>
270+ <LoadingButton
271+ onclick={handleSubmit}
272+ disabled={!currentUser}
273+ loading={isSendingMessage}
274+ text="Send"
275+ />
266276 </div>
267277 </div>
268278
@@ -291,11 +301,12 @@ function handleMaximizeEvent(event: CustomEvent<{
291301 {/if}
292302
293303 <style>
294- :global(*) {
304+
305+ .chat-area, .users-list, input {
295306 font-size: 1rem;
296- font-family: 'Microsoft Sans Serif', 'Segoe UI', Tahoma, sans-serif;
307+ font-family: 'Microsoft Sans Serif', 'Segoe UI', Arial, Tahoma, sans-serif;
297308 }
298-
309+
299310 .title-bar {
300311 height: 2rem;
301312 position: relative;
@@ -389,7 +400,7 @@ function handleMaximizeEvent(event: CustomEvent<{
389400 }
390401
391402 .input-container input:disabled,
392- .input-container button:disabled {
403+ .input-container {
393404 opacity: 0.7;
394405 background-color: rgba(128, 128, 128, 0.1);
395406 cursor: not-allowed;
@@ -6,6 +6,7 @@ import { browser } from '$app/environment';
6 import { draggable } from '$lib/actions/draggable';6 import { draggable } from '$lib/actions/draggable';
7 import { resizable } from '$lib/actions/resizable';7 import { resizable } from '$lib/actions/resizable';
8 import { maximizable } from '$lib/actions/maximizable';8 import { maximizable } from '$lib/actions/maximizable';
9+import LoadingButton from './ui/button-loading.svelte';
9 10
10 // Initialize with default values for SSR11 // Initialize with default values for SSR
11 let windowWidth = $state(400);12 let windowWidth = $state(400);
@@ -131,16 +132,20 @@ onMount(() => {
131 return () => window.removeEventListener('resize', handleResize);132 return () => window.removeEventListener('resize', handleResize);
132 });133 });
133 134
135+// New reactive state to track when a message is being sent
136+let isSendingMessage = $state(false);
137+
134 async function handleSubmit() {138 async function handleSubmit() {
135 if (!currentMessage.trim()) return;139 if (!currentMessage.trim()) return;
136- 140+ isSendingMessage = true;
137 try {141 try {
138 await chatState.sendMessage(currentMessage);142 await chatState.sendMessage(currentMessage);
139 currentMessage = '';143 currentMessage = '';
140 } catch (error) {144 } catch (error) {
141 console.error('Failed to send message:', error);145 console.error('Failed to send message:', error);
142- // You might want to show this error to the user in a more friendly way
143 alert('Failed to send message. Please try again.');146 alert('Failed to send message. Please try again.');
147+ } finally {
148+ isSendingMessage = false;
144 }149 }
145 }150 }
146 151
@@ -262,7 +267,12 @@ function handleMaximizeEvent(event: CustomEvent<{
262 placeholder="Type a message..."267 placeholder="Type a message..."
263 disabled={!currentUser}268 disabled={!currentUser}
264 />269 />
265- <button onclick={handleSubmit} disabled={!currentUser}>Send</button>270+ <LoadingButton
271+ onclick={handleSubmit}
272+ disabled={!currentUser}
273+ loading={isSendingMessage}
274+ text="Send"
275+ />
266 </div>276 </div>
267 </div>277 </div>
268 278
@@ -291,11 +301,12 @@ function handleMaximizeEvent(event: CustomEvent<{
291 {/if}301 {/if}
292 302
293 <style>303 <style>
294- :global(*) {304+
305+ .chat-area, .users-list, input {
295 font-size: 1rem;306 font-size: 1rem;
296- font-family: 'Microsoft Sans Serif', 'Segoe UI', Tahoma, sans-serif;307+ font-family: 'Microsoft Sans Serif', 'Segoe UI', Arial, Tahoma, sans-serif;
297 }308 }
298-309+
299 .title-bar {310 .title-bar {
300 height: 2rem;311 height: 2rem;
301 position: relative;312 position: relative;
@@ -389,7 +400,7 @@ function handleMaximizeEvent(event: CustomEvent<{
389 }400 }
390 401
391 .input-container input:disabled,402 .input-container input:disabled,
392- .input-container button:disabled {403+ .input-container {
393 opacity: 0.7;404 opacity: 0.7;
394 background-color: rgba(128, 128, 128, 0.1);405 background-color: rgba(128, 128, 128, 0.1);
395 cursor: not-allowed;406 cursor: not-allowed;
A src/lib/components/ui/button-loading.svelte +77 -0
@@ -0,0 +1,77 @@
1+<script lang="ts">
2+ import { onDestroy } from 'svelte';
3+
4+ // Props with defaults:
5+ // - text: button label when not loading (default "Send")
6+ // - loading: indicates if the button should show a spinner (default false)
7+ // - spinnerChar: the character to use for the spinner animation (default '.')
8+ let { onclick, disabled = $bindable(false), text, loading = $bindable(false), spinnerChar = $bindable('.') } = $props();
9+
10+ // Using $state for internal reactive variables.
11+ let animatedDots = $state(''); // will hold the animated spinner text
12+ let count = $state(1);
13+ let isDecreasing = $state(true);
14+ let intervalID = $state<number | null>(null);
15+
16+ // Effect that starts/stops the spinner based on loading state.
17+ $effect(() => {
18+ if (loading) {
19+ console.debug("LoadingButton: Spinner started");
20+ // Immediately update spinner text to show 3 dots and start decreasing
21+ animatedDots = spinnerChar.repeat(3);
22+ count = 3;
23+ isDecreasing = true;
24+
25+ if (!intervalID) {
26+ intervalID = window.setInterval(() => {
27+ // Ping-pong logic: 3,2,1,2,3...
28+ count = count + (isDecreasing ? -1 : 1);
29+ if (count <= 1) {
30+ count = 1;
31+ isDecreasing = false;
32+ } else if (count >= 3) {
33+ count = 3;
34+ isDecreasing = true;
35+ }
36+ animatedDots = spinnerChar.repeat(count);
37+ console.debug("LoadingButton: Animated dots updated to", animatedDots);
38+ }, 500);
39+ }
40+ } else {
41+ console.debug("LoadingButton: Spinner stopped");
42+ if (intervalID) {
43+ clearInterval(intervalID);
44+ intervalID = null;
45+ }
46+ // When not loading, display the default text.
47+ animatedDots = text;
48+ }
49+ });
50+
51+ onDestroy(() => {
52+ if (intervalID) {
53+ console.debug("LoadingButton: onDestroy clearing interval");
54+ clearInterval(intervalID);
55+ }
56+ });
57+</script>
58+
59+<button onclick={onclick} disabled={disabled}>
60+ {#if loading}
61+ <span>{animatedDots}</span>
62+ {:else}
63+ <span>{text}</span>
64+ {/if}
65+</button>
66+
67+<style>
68+ button {
69+ padding: 0.5rem 1rem;
70+ font-size: 1rem;
71+ cursor: pointer;
72+ }
73+ button:disabled {
74+ cursor: not-allowed;
75+ opacity: 0.7;
76+ }
77+</style> ⏎̸
@@ -0,0 +1,77 @@
1+<script lang="ts">
2+ import { onDestroy } from 'svelte';
3+
4+ // Props with defaults:
5+ // - text: button label when not loading (default "Send")
6+ // - loading: indicates if the button should show a spinner (default false)
7+ // - spinnerChar: the character to use for the spinner animation (default '.')
8+ let { onclick, disabled = $bindable(false), text, loading = $bindable(false), spinnerChar = $bindable('.') } = $props();
9+
10+ // Using $state for internal reactive variables.
11+ let animatedDots = $state(''); // will hold the animated spinner text
12+ let count = $state(1);
13+ let isDecreasing = $state(true);
14+ let intervalID = $state<number | null>(null);
15+
16+ // Effect that starts/stops the spinner based on loading state.
17+ $effect(() => {
18+ if (loading) {
19+ console.debug("LoadingButton: Spinner started");
20+ // Immediately update spinner text to show 3 dots and start decreasing
21+ animatedDots = spinnerChar.repeat(3);
22+ count = 3;
23+ isDecreasing = true;
24+
25+ if (!intervalID) {
26+ intervalID = window.setInterval(() => {
27+ // Ping-pong logic: 3,2,1,2,3...
28+ count = count + (isDecreasing ? -1 : 1);
29+ if (count <= 1) {
30+ count = 1;
31+ isDecreasing = false;
32+ } else if (count >= 3) {
33+ count = 3;
34+ isDecreasing = true;
35+ }
36+ animatedDots = spinnerChar.repeat(count);
37+ console.debug("LoadingButton: Animated dots updated to", animatedDots);
38+ }, 500);
39+ }
40+ } else {
41+ console.debug("LoadingButton: Spinner stopped");
42+ if (intervalID) {
43+ clearInterval(intervalID);
44+ intervalID = null;
45+ }
46+ // When not loading, display the default text.
47+ animatedDots = text;
48+ }
49+ });
50+
51+ onDestroy(() => {
52+ if (intervalID) {
53+ console.debug("LoadingButton: onDestroy clearing interval");
54+ clearInterval(intervalID);
55+ }
56+ });
57+</script>
58+
59+<button onclick={onclick} disabled={disabled}>
60+ {#if loading}
61+ <span>{animatedDots}</span>
62+ {:else}
63+ <span>{text}</span>
64+ {/if}
65+</button>
66+
67+<style>
68+ button {
69+ padding: 0.5rem 1rem;
70+ font-size: 1rem;
71+ cursor: pointer;
72+ }
73+ button:disabled {
74+ cursor: not-allowed;
75+ opacity: 0.7;
76+ }
77+</style> ⏎̸
M src/lib/types/chat.ts +0 -5
@@ -1,8 +1,3 @@
1-/**
2- * TypeScript interfaces for an AOL Instant Messenger–like project.
3- * Each interface represents a table structure in your database.
4- */
5-
61 // Users table: holds authentication and profile details
72 export interface User {
83 id: string;
@@ -1,8 +1,3 @@
1-/**
2- * TypeScript interfaces for an AOL Instant Messenger–like project.
3- * Each interface represents a table structure in your database.
4- */
5-
6 // Users table: holds authentication and profile details1 // Users table: holds authentication and profile details
7 export interface User {2 export interface User {
8 id: string;3 id: string;
M vite.config.ts +10 -0
@@ -6,5 +6,15 @@ export default defineConfig({
66
77 test: {
88 include: ['src/**/*.{test,spec}.{js,ts}']
9+ },
10+ build: {
11+ minify: 'terser',
12+ terserOptions: {
13+ compress: {
14+ pure_funcs: [
15+ 'console.debug',
16+ ]
17+ }
18+ },
919 }
1020 });
@@ -6,5 +6,15 @@ export default defineConfig({
6 6
7 test: {7 test: {
8 include: ['src/**/*.{test,spec}.{js,ts}']8 include: ['src/**/*.{test,spec}.{js,ts}']
9+ },
10+ build: {
11+ minify: 'terser',
12+ terserOptions: {
13+ compress: {
14+ pure_funcs: [
15+ 'console.debug',
16+ ]
17+ }
18+ },
9 }19 }
10 });20 });

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