From 0fc2128e924874d990b47deab504004d6490ff9b Mon Sep 17 00:00:00 2001 From: Adam Lamers Date: Sat, 4 Apr 2026 22:51:27 -0400 Subject: [PATCH] deduplicate logic from message list and thread message list --- src/SpacetimeProvider.svelte | 5 - src/auth/AuthGate.svelte | 2 +- src/chat/components/MessageItem.svelte | 371 +++++++++++++++++++ src/chat/components/MessageList.svelte | 323 +--------------- src/chat/components/ThreadMessageList.svelte | 354 ++---------------- src/config.ts | 19 +- src/connection.svelte.ts | 6 +- vite.config.ts | 4 +- wrangler.jsonc | 12 +- 9 files changed, 436 insertions(+), 660 deletions(-) create mode 100644 src/chat/components/MessageItem.svelte diff --git a/src/SpacetimeProvider.svelte b/src/SpacetimeProvider.svelte index a401904..b708603 100644 --- a/src/SpacetimeProvider.svelte +++ b/src/SpacetimeProvider.svelte @@ -17,11 +17,6 @@ // We store the db instance in state so it can be updated inside an effect let db = $state(null); - function triggerReconnect() { - console.log("SpacetimeProvider: Reconnect requested, incrementing key..."); - reconnectKey++; - } - // Use an effect to handle the connection lifecycle. // This avoids state_unsafe_mutation since we are now in an effect. $effect(() => { diff --git a/src/auth/AuthGate.svelte b/src/auth/AuthGate.svelte index 3c3c2f7..0924fec 100644 --- a/src/auth/AuthGate.svelte +++ b/src/auth/AuthGate.svelte @@ -5,7 +5,7 @@ import { TokenStore, getStdbHost, getStdbDbName } from "../config"; import SpacetimeProvider from "../SpacetimeProvider.svelte"; - let { children, showServerSettings, onToggleServerSettings } = $props<{ + let { children, showServerSettings, onToggleServerSettings: _onToggleServerSettings } = $props<{ children: any, showServerSettings: boolean, onToggleServerSettings: (_val: boolean) => void diff --git a/src/chat/components/MessageItem.svelte b/src/chat/components/MessageItem.svelte new file mode 100644 index 0000000..921345a --- /dev/null +++ b/src/chat/components/MessageItem.svelte @@ -0,0 +1,371 @@ + + + + +
+ {#if !isThread} +
+ {#if chat.isFullyAuthenticated} +
+ + {#if showPicker && pickerPos} +
+ { + chat.handleToggleReaction(msg.id, emoji, customId); + showPicker = false; + pickerPos = null; + }} + /> +
+ {/if} +
+ {/if} + {#if !existingThread && chat.isFullyAuthenticated} + + {/if} +
+ {/if} + + u.identity.isEqual(msg.sender))} size={isThread ? "small" : "medium"} class="message-avatar" /> + +
+
+ {msgUsername} + {#if !isThread} + {chat.formatTime(msg.sent)} + {/if} +
+ +
+ +
+ + {#if messageImages.length > 0} +
+ + + {#if !collapsedImages} +
+
+ {#each messageImages as mi} + {@const image = chat.images.find(img => img.id === mi.imageId)} + {#if image} + +
chat.viewingImageId = image.id} + role="button" + tabindex="0" + onkeydown={(e) => e.key === 'Enter' && (chat.viewingImageId = image.id)} + aria-label="View full resolution image" + > + Uploaded +
+ {/if} + {/each} +
+
+ {/if} +
+ {/if} + +
+ {#each Object.entries(emojiGroups) as [key, group]} + {@const hasReacted = group.some(r => r.identity.isEqual(chat.identity))} + {@const emoji = group[0].emoji} + {@const customEmojiId = group[0].customEmojiId} + + {/each} + + {#if isThread && chat.isFullyAuthenticated} +
+ + {#if showPicker && pickerPos} +
+ { + chat.handleToggleReaction(msg.id, emoji, customId); + showPicker = false; + pickerPos = null; + }} + /> +
+ {/if} +
+ {/if} +
+ + {#if !isThread && existingThread} + {@const threadMessageCount = chat.allMessages.filter(m => m.threadId === existingThread.id).length} + + {/if} +
+
+ +{#if tooltip.visible} +
+ {tooltip.text} +
+{/if} + + \ No newline at end of file diff --git a/src/chat/components/MessageList.svelte b/src/chat/components/MessageList.svelte index 78fc1ad..3638201 100644 --- a/src/chat/components/MessageList.svelte +++ b/src/chat/components/MessageList.svelte @@ -1,13 +1,7 @@ - -
{#each chat.channelMessages as msg (msg.id.toString())} - {@const msgUsername = chat.getUsername(msg.sender)} - {@const existingThread = chat.allThreads.find((t) => t.parentMessageId === msg.id)} - {@const reactions = chat.messageReactions.filter(r => r.messageId === msg.id)} - {@const emojiGroups = reactions.reduce((acc, r) => { - const key = r.emoji || `custom:${r.customEmojiId}`; - if (!acc[key]) acc[key] = []; - acc[key].push(r); - return acc; - }, {} as Record)} {@const isHighlighted = (chat.pendingThreadParentMessageId === msg.id) || (chat.activeThread?.parentMessageId === msg.id)} - {@const messageImages = chat.messageImages.filter(mi => mi.messageId === msg.id)} - -
-
- {#if chat.isFullyAuthenticated} -
- - {#if activePickerMessageId === msg.id && pickerPos} -
- { - chat.handleToggleReaction(msg.id, emoji, customId); - activePickerMessageId = null; - pickerPos = null; - }} - onClose={() => { - activePickerMessageId = null; - pickerPos = null; - }} - /> -
- {/if} -
- {/if} - {#if !existingThread && chat.isFullyAuthenticated} - - {/if} -
- - u.identity.isEqual(msg.sender))} class="message-avatar" /> -
-
- {msgUsername} - {chat.formatTime(msg.sent)} -
-
- -
- - {#if messageImages.length > 0} -
- - - {#if !collapsedImages[msg.id.toString()]} -
-
- {#each messageImages as mi} - {@const image = chat.images.find(img => img.id === mi.imageId)} - {#if image} - -
chat.viewingImageId = image.id} - role="button" - tabindex="0" - onkeydown={(e) => e.key === 'Enter' && (chat.viewingImageId = image.id)} - aria-label="View full resolution image" - > - Uploaded -
- {/if} - {/each} -
-
- {/if} -
- {/if} - -
- {#each Object.entries(emojiGroups) as [key, group]} - {@const hasReacted = group.some(r => r.identity.isEqual(chat.identity))} - {@const emoji = group[0].emoji} - {@const customEmojiId = group[0].customEmojiId} - - {/each} -
- {#if existingThread} - {@const threadMessageCount = chat.allMessages.filter(m => m.threadId === existingThread.id).length} - - {/if} -
-
+ {/each}
- {#if tooltip.visible} -
- {tooltip.text} -
- {/if} - {#if !isAtBottom && chat.channelMessages.length > 0}
You're viewing older messages @@ -378,10 +105,6 @@
diff --git a/src/chat/components/ThreadMessageList.svelte b/src/chat/components/ThreadMessageList.svelte index faca5e2..80460e3 100644 --- a/src/chat/components/ThreadMessageList.svelte +++ b/src/chat/components/ThreadMessageList.svelte @@ -1,24 +1,18 @@ - + $effect(() => { + // Track threadMessages and activeThreadId + const messages = threadMessages; + const currentThreadId = chat.activeThreadId; + + if (messages.length > 0 && threadMessagesEndRef) { + const isThreadSwitch = currentThreadId !== lastThreadId; + lastThreadId = currentThreadId; + + if (isThreadSwitch || isAtBottom) { + scrollToBottom(!isThreadSwitch); + } + } + }); +
{ onscroll={handleScroll} > {#each threadMessages as msg (msg.id.toString())} - {@const msgUsername = getUsername(msg.sender, users)} - {@const messageImages = chat.messageImages.filter(mi => mi.messageId === msg.id)} - {@const reactions = chat.messageReactions.filter(r => r.messageId === msg.id)} - {@const emojiGroups = reactions.reduce((acc, r) => { - const key = r.emoji || `custom:${r.customEmojiId}`; - if (!acc[key]) acc[key] = []; - acc[key].push(r); - return acc; - }, {} as Record)} -
- u.identity.isEqual(msg.sender))} size="small" class="message-avatar" /> -
-
- {msgUsername} -
-
- -
- - {#if messageImages.length > 0} -
- - - {#if !collapsedImages[msg.id.toString()]} -
-
- {#each messageImages as mi} - {@const image = chat.images.find(img => img.id === mi.imageId)} - {#if image} - -
chat.viewingImageId = image.id} - role="button" - tabindex="0" - onkeydown={(e) => e.key === 'Enter' && (chat.viewingImageId = image.id)} - aria-label="View full resolution image" - > - Uploaded -
- {/if} - {/each} -
-
- {/if} -
- {/if} -
- {#each Object.entries(emojiGroups) as [key, group]} - {@const hasReacted = group.some(r => r.identity.isEqual(chat.identity))} - {@const emoji = group[0].emoji} - {@const customEmojiId = group[0].customEmojiId} - - {/each} - - {#if chat.isFullyAuthenticated} -
- - {#if activePickerMessageId === msg.id && pickerPos} -
- { - chat.handleToggleReaction(msg.id, emoji, customId); - activePickerMessageId = null; - pickerPos = null; - }} - onClose={() => { - activePickerMessageId = null; - pickerPos = null; - }} - /> -
- {/if} -
- {/if} -
-
-
+ {/each}
- {#if tooltip.visible} -
- {tooltip.text} -
- {/if} - {#if !isAtBottom && threadMessages.length > 0}
Older messages @@ -401,38 +147,4 @@ $effect(() => { from { transform: translate(-50%, -10px); opacity: 0; } to { transform: translate(-50%, 0); opacity: 1; } } - - .custom-emoji-reaction { - width: 16px; - height: 16px; - object-fit: contain; - vertical-align: middle; - } - - .message-reaction-tooltip { - position: fixed; - transform: translate(-50%, -100%); - margin-top: -8px; - background-color: var(--background-floating); - color: var(--text-normal); - padding: 6px 12px; - border-radius: 4px; - font-size: 0.85rem; - pointer-events: none; - z-index: 10000; - white-space: nowrap; - box-shadow: var(--elevation-stroke), var(--elevation-high); - border: 1px solid var(--background-modifier-accent); - font-family: inherit; - } - - .message-reaction-tooltip::after { - content: ""; - position: absolute; - top: 100%; - left: 50%; - transform: translateX(-50%); - border: 6px solid transparent; - border-top-color: var(--background-floating); - } diff --git a/src/config.ts b/src/config.ts index a62cfa5..cdd84aa 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,9 +5,16 @@ import { connectionState } from "./connection.svelte"; export { HOST_KEY, DB_NAME_KEY, getEnv }; -const normalizeHost = (host: string) => { +export const normalizeHost = (host: string) => { try { - const url = new URL(host.includes("://") ? host : `https://${host}`); + // If it's localhost and has no protocol, default to ws:// instead of wss:// or https:// + const defaultProtocol = + host.includes("localhost") || host.includes("127.0.0.1") + ? "ws://" + : "wss://"; + const url = new URL( + host.includes("://") ? host : `${defaultProtocol}${host}`, + ); return url.origin.replace(/^http/, "ws"); // Ensure ws/wss } catch { return host.trim().replace(/\/+$/, ""); // Fallback @@ -122,15 +129,15 @@ class ConnectionManager { this.#reconnectTimeout = null; if (this.#isStopped) return; this.#retryCount++; - console.log("ConnectionManager: Reconnect delay reached. Reloading window..."); + console.log( + "ConnectionManager: Reconnect delay reached. Reloading window...", + ); window.location.reload(); }, delay); }; } -export const connectionBuilder = ( - oidcToken?: string, -) => { +export const connectionBuilder = (oidcToken?: string) => { const host = getStdbHost(); const dbName = getStdbDbName(); diff --git a/src/connection.svelte.ts b/src/connection.svelte.ts index e677480..7eb12c4 100644 --- a/src/connection.svelte.ts +++ b/src/connection.svelte.ts @@ -1,6 +1,10 @@ // src/connection.svelte.ts -export type ConnectionStatus = "idle" | "connecting" | "connected" | "disconnected"; +export type ConnectionStatus = + | "idle" + | "connecting" + | "connected" + | "disconnected"; class ConnectionState { status = $state("idle"); diff --git a/vite.config.ts b/vite.config.ts index faa54f2..7bc1f97 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ plugins: [ process.env.VITE_USE_SSL === "true" ? basicSsl() : [], svelte(), - cloudflare() + cloudflare(), ], // Prevent vite from obscuring rust errors clearScreen: false, @@ -20,4 +20,4 @@ export default defineConfig({ port: process.env.VITE_USE_SSL === "true" ? 5174 : 5173, host: "0.0.0.0", }, -}); \ No newline at end of file +}); diff --git a/wrangler.jsonc b/wrangler.jsonc index 55a905a..71e1dc5 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -3,16 +3,14 @@ "name": "ditchcord", "compatibility_date": "2026-04-04", "observability": { - "enabled": true + "enabled": true, }, "assets": { "directory": "dist", - "not_found_handling": "single-page-application" + "not_found_handling": "single-page-application", }, - "compatibility_flags": [ - "nodejs_compat" - ], + "compatibility_flags": ["nodejs_compat"], "build": { - "command": "pnpm run build" - } + "command": "pnpm run build", + }, }