From 84e778d5cdd1bb5cded8a7c0238797c17d48666c Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sun, 16 Aug 2026 15:29:11 +0200 Subject: feat(hub): chat, presence, a Profile page, and downloads that do not freeze MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chat opens on the newest hundred messages, loads fifty older on demand with the reading position anchored — the distance from the *bottom*, since everything above the viewport just grew — and follows new messages only when the reader was already at the end. Day separators, sender grouping, an unread marker, and a jump-to-latest pill. Messages are keyed by id: index keys plus prepending makes Preact reuse the wrong bubbles. A presence dot per group in the sidebar, three states, each backed by something: the hub's registry, or a connection this browser made or failed to make. Never colour alone — red and green are the pair colour-blind readers cannot separate — so each dot carries a title and an aria-label. Profile is split out of Settings: identity, node link, pinned node identities and account deletion. Mixing them put an irreversible button two scrolls under a theme picker. The create-group page loses its centred 520 px card, which left 190 px of margin either side, and its two button panels become a radio group — a button conveys no chosen state to a screen reader, and side by side they read as two independent actions rather than one either/or. The Files toolbar shows its actions as icon buttons the moment Select is on, disabled when they do not apply rather than appearing and vanishing. On a phone the right-hand group could not wrap and ran 130 px off the screen. Streamed downloads no longer freeze after one chunk. `registration.active` says a worker exists, not that this page is controlled by it — and an uncontrolled page's requests never reach its fetch handler, so the worker took the stream and was never asked for it, leaving `writer.write()` waiting on backpressure that would never lift. The page now requires control and the worker confirms it actually served the request before the sink is trusted. Fixed on the way: `setActionsOpen` outlived the state it belonged to and threw on every Files action; the chat scrollbar stopped short of the bottom; the owner's row sat lower than the rest; About showed a version hardcoded two releases ago. Co-Authored-By: Claude Opus 5 --- packages/meshbay-hub/src/meshbay_hub/static/app.js | 770 +++++++++++++++------ .../src/meshbay_hub/static/downloads.js | 46 +- .../src/meshbay_hub/static/locales/de.js | 17 + .../src/meshbay_hub/static/locales/en.js | 17 + .../src/meshbay_hub/static/locales/es.js | 17 + .../src/meshbay_hub/static/locales/fr.js | 17 + .../src/meshbay_hub/static/locales/it.js | 17 + .../src/meshbay_hub/static/locales/ja.js | 17 + .../src/meshbay_hub/static/locales/nl.js | 17 + .../src/meshbay_hub/static/locales/pl.js | 17 + .../src/meshbay_hub/static/locales/pt-BR.js | 17 + .../src/meshbay_hub/static/locales/zh-CN.js | 17 + .../meshbay-hub/src/meshbay_hub/static/style.css | 306 ++++++-- packages/meshbay-hub/src/meshbay_hub/static/sw.js | 8 + .../src/meshbay_hub/static/transport.js | 99 ++- packages/meshbay-hub/tests/test_downloads.py | 41 +- .../meshbay-hub/tests/test_layout_responsive.py | 159 +++++ packages/meshbay-hub/tests/test_spa_ordering.py | 7 +- .../meshbay-hub/tests/test_transport_contracts.py | 218 ++++++ .../meshbay-hub/tests/test_video_stream_switch.py | 160 +++++ 20 files changed, 1693 insertions(+), 291 deletions(-) create mode 100644 packages/meshbay-hub/tests/test_layout_responsive.py create mode 100644 packages/meshbay-hub/tests/test_transport_contracts.py create mode 100644 packages/meshbay-hub/tests/test_video_stream_switch.py (limited to 'packages') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 6a5a13f..7968284 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -1,5 +1,5 @@ import { - html, render, useState, useEffect, useCallback, useRef, + html, render, useState, useEffect, useLayoutEffect, useCallback, useRef, createContext, useContext, } from './vendor/htm-preact.js'; import { t, getLocale, setLocale, initLocale, LOCALES } from './i18n.js'; @@ -245,6 +245,16 @@ const ICON_PATHS = { globe: ['M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18', 'M3.4 9.2h17.2M3.4 14.8h17.2', 'M12 3c-2.6 2.4-4 5.6-4 9s1.4 6.6 4 9c2.6-2.4 4-5.6 4-9s-1.4-6.6-4-9'], + archive: ['M3 7.5h18v3H3z', 'M4.5 10.5V19a1.5 1.5 0 0 0 1.5 1.5h12a1.5 1.5 0 0 0 1.5-1.5v-8.5', + 'M10 14h4'], + play: ['M8 5.5v13l11-6.5z'], + eye: ['M2 12s3.6-6.5 10-6.5S22 12 22 12s-3.6 6.5-10 6.5S2 12 2 12', + 'M12 14.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5'], + trash: ['M4 7h16', 'M10 11v6M14 11v6', + 'M6 7l1 12.5A1.5 1.5 0 0 0 8.5 21h7a1.5 1.5 0 0 0 1.5-1.5L18 7', + 'M9.5 7V5a1.5 1.5 0 0 1 1.5-1.5h2A1.5 1.5 0 0 1 14.5 5v2'], + user: ['M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8', + 'M4.5 20a7.5 7.5 0 0 1 15 0'], gear: ['M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6', 'M19.2 14.4a1.7 1.7 0 0 0 .3 1.9 2 2 0 1 1-2.8 2.8 1.7 1.7 0 0 0-2.9 1.2 2 2 0 0 1-4 0 1.7 1.7 0 0 0-2.9-1.2 2 2 0 1 1-2.8-2.8 1.7 1.7 0 0 0-1.2-2.9 2 2 0 0 1 0-4 1.7 1.7 0 0 0 1.2-2.9 2 2 0 1 1 2.8-2.8 1.7 1.7 0 0 0 2.9-1.2 2 2 0 0 1 4 0 1.7 1.7 0 0 0 2.9 1.2 2 2 0 1 1 2.8 2.8 1.7 1.7 0 0 0 1.2 2.9 2 2 0 0 1 0 4 1.7 1.7 0 0 0-1.5 1.1z'], sun: ['M12 8.2a3.8 3.8 0 1 0 0 7.6 3.8 3.8 0 0 0 0-7.6', @@ -335,6 +345,9 @@ function UserMenu({ user, theme, onThemeChange, onLogout }) { && html`<${Icon} name="check" cls="umi-check" />`} `)} + @@ -473,7 +486,7 @@ function Nav({ user, theme, onThemeChange, onLogout, onMenuToggle, unreadCount } // ── Sidebar ────────────────────────────────────────────────────────────────── -function Sidebar({ groups, route, menuOpen, role }) { +function Sidebar({ groups, presence, route, menuOpen, role }) { const isStaff = role === 'moderator' || role === 'admin'; return html` @@ -842,21 +867,20 @@ function CreateGroupPage({ token, onCreated }) { }; return html` -
-
-
-

${t('create_group.title')}

-

${t('create_group.hint')}

-
- ${error && html`
${error}
`} -
+
+

${t('create_group.title')}

+

${t('create_group.hint')}

+ ${error && html`
${error}
`} + + +
setName(e.target.value)} required autofocus />
-
+