From 25106335389296f95ff357d408952051bb799f23 Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Thu, 1 Oct 2026 23:07:24 +0200 Subject: [PATCH] Images in chat and descriptions open large on click The channel description preview stays open underneath while an image from it is shown. Co-Authored-By: Claude Opus 5.5 --- src/App.svelte | 15 ++++++++++++++- src/app.css | 4 ++++ src/lib/ui.svelte.ts | 2 ++ src/ui/ChannelNode.svelte | 16 ++++++++++++++-- src/ui/ImageViewer.svelte | 25 +++++++++++++++++++++++++ test/e2e/app.e2e.ts | 13 +++++++++++++ 6 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 src/ui/ImageViewer.svelte diff --git a/src/App.svelte b/src/App.svelte index b12d3da..e5ef036 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -15,6 +15,7 @@ import ChannelDialog from './ui/ChannelDialog.svelte'; import PublicServers from './ui/PublicServers.svelte'; import ServerInfoDialog from './ui/ServerInfoDialog.svelte'; + import ImageViewer from './ui/ImageViewer.svelte'; import ShareDialog from './ui/ShareDialog.svelte'; import { share } from './lib/share.svelte.ts'; import ResizeHandle from './ui/ResizeHandle.svelte'; @@ -63,13 +64,24 @@ else ui.panelOpen = false; } + // Images in rendered text (messages, descriptions, the quick preview) and image embeds open + // large on click. Not in editors, link preview cards or the YouTube card. + function imageClick(e: MouseEvent) { + if (e.button !== 0 || !(e.target instanceof HTMLImageElement)) return; + const img = e.target; + if (!img.closest('.html, .embed') || img.closest('[contenteditable], .rich, .card, .yt')) return; + e.preventDefault(); + e.stopPropagation(); + ui.image = img.currentSrc || img.src; + } + const closeOverlays = () => { navOpen = false; if (!panelInline) ui.panelOpen = false; }; - + {#snippet rail()} (editing = { server: null })} onedit={s => (editing = { server: s })} onbrowse={() => (ui.publicServers = true)} @@ -134,6 +146,7 @@ {/if} {#if share.dialog} (share.dialog = null)} />{/if} {#if ui.prompt} { const p = ui.prompt; ui.prompt = null; p?.oncancel?.(); }} />{/if} + diff --git a/test/e2e/app.e2e.ts b/test/e2e/app.e2e.ts index 1eee29b..7413c5c 100644 --- a/test/e2e/app.e2e.ts +++ b/test/e2e/app.e2e.ts @@ -856,6 +856,19 @@ try { assert.ok(inline.length <= 131072, `inline image within the server limit (${inline.length})`); console.log(`ok: inline image without upload host (${inline.length} chars)`); + // A click on an image in the chat shows it large; Escape or a click closes it + await page.locator('.chat .msg .html img').last().click(); + const viewer = page.getByRole('dialog', { name: 'Image' }); + await viewer.waitFor(); + const shown = await viewer.locator('img').evaluate((i: HTMLImageElement) => [i.naturalWidth > 0, i.getBoundingClientRect().width]); + assert.ok(shown[0] && (shown[1] as number) > 300, `image shown large: ${shown}`); + await page.keyboard.press('Escape'); + await viewer.waitFor({ state: 'detached' }); + await page.locator('.chat .msg .html img').last().click(); + await viewer.getByRole('button', { name: 'Close image' }).click({ position: { x: 5, y: 5 } }); + await viewer.waitFor({ state: 'detached' }); + console.log('ok: images open large on click'); + // ─── Several servers at once ───────────────────────────────────────────── const userState = (name: string) => [...bob.users.values()].find(u => u.name === name); const until2 = (label: string, cond: () => boolean) => within(new Promise(res => {