<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-hub/src/meshbay_hub/static/chat-app.js, branch 0.11</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.11</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.11'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-09-01T21:34:49Z</updated>
<entry>
<title>fix(hub): let the reader scroll up in the chat again</title>
<updated>2026-09-01T21:34:49Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-01T21:34:49Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=32855a95e11032302f8d24036f6f2dd44b829368'/>
<id>urn:sha1:32855a95e11032302f8d24036f6f2dd44b829368</id>
<content type='text'>
The chat could not be read back: any wheel gesture was undone in the frame
it happened in, and the "jump to latest" button never appeared.

None of the pins in ChatPanel are at fault -- every one of them is guarded
by "only if the reader is at the bottom". The reader never got to stop
being at the bottom.

fit() set the panel's height, read documentElement.scrollHeight back and
subtracted the overflow, so the document alternately did and did not
overflow the window. The page scrollbar appeared and vanished with it and
visualViewport fired resize at every pass -- the event fit() is bound to.
It therefore re-entered itself for the life of the panel: measured at 240
firings in two seconds on a page nobody was touching, against 2 for a bare
document. Each pass ran fitAndPin, which re-pinned the list to the bottom
before the scroll event that would have recorded the gesture was delivered
a frame later, so atBottomRef never went false.

- fit() learns the space below the panel once and remembers it on the
  element instead of re-deriving it by writing and measuring back. At the
  steady state it writes nothing, so it produces no resize. A real window
  resize or an orientation change forgets the learnt value and measures
  again (the page under the panel may have reflowed); visualViewport
  deliberately does not, since a phone fires it constantly.

- The scroll-to-bottom is now scoped to *arrival*, which is all it was ever
  for: opening the group, or coming back to the Chat tab, including the
  thumbnails and link-preview cards that keep growing the list for a second
  afterwards. It ends when the reader takes hold of the scroll, and the
  ResizeObserver disconnects there.

- That release is recorded from the gesture (wheel/touchmove/pointerdown/
  keydown), not from the scroll event, which arrives too late to protect
  anything.

Unchanged: landing on the newest message, following new messages while
already at the bottom, the "load older" anchor and the unread marker.

tests/harness/chat_scroll_probe.py mounts the real ChatPanel in a browser
and reads a conversation back; test_chat_scroll_up.py asserts against it.
With the fix reverted, five of its six tests fail and the sixth -- landing
on the newest message -- still passes, which is the property that must not
have been traded away. A structural test cannot see any of this, which is
why it is measured.

test_layout_responsive.py pinned the listener's name and follows the
rename.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01W8oRqEHhnKUr1NfmTVdcyL
</content>
</entry>
<entry>
<title>fix(hub): chat scroll-to-bottom, message loading on group switch, connecting spinner</title>
<updated>2026-08-29T11:52:01Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-29T11:52:01Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=c4813d1aaf1f4fdf3eb9dc07909f324f5745544d'/>
<id>urn:sha1:c4813d1aaf1f4fdf3eb9dc07909f324f5745544d</id>
<content type='text'>
Three regressions fixed in ChatPanel:

- Scroll pinning: layout effect now depends on [messages, hasMore] so the
  "load older" button appearance triggers a re-pin; setHasMore is called
  before setMessages to avoid an intermediate render without the button;
  fit() is wrapped in fitAndPin() so panel resizing re-pins the scroll.

- Message loading on group switch: fetch effect depends on the status prop
  instead of transportRef.current?.connected to avoid racing with
  GroupPage's cleanup; loadedRef resets on fetch failure so a retry works.

- Connecting spinner restored in the Chat tab empty state.

Adds structural regression tests (test_chat_scroll_bottom.py) that lock
the dependency arrays and setState ordering so these invariants break
loudly in CI.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(chat): focus the composer when the Chat tab opens</title>
<updated>2026-08-28T06:34:41Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-28T06:34:41Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=45cbe43a21a9d046afebb248b4bd455bf0fd9cd0'/>
<id>urn:sha1:45cbe43a21a9d046afebb248b4bd455bf0fd9cd0</id>
<content type='text'>
ChatPanel remounts on every tab switch, so a mount effect is the tab-entry
hook. `focus({ preventScroll: true })` so it does not fight the panel-sizing
and scroll-to-bottom effects still settling, and so a phone opens the
keyboard without the view jumping.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018gKJ85aZyvEwarXMFzFEwi
</content>
</entry>
<entry>
<title>feat(chat): link previews for pasted URLs</title>
<updated>2026-08-28T01:43:19Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-28T01:43:19Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=ce4e10c4b8bd9c66c375c3a5d5c18d8552655775'/>
<id>urn:sha1:ce4e10c4b8bd9c66c375c3a5d5c18d8552655775</id>
<content type='text'>
Paste an http(s) link in a group's chat and it unfurls into an OpenGraph
card — title, description, site name, and image — the way WhatsApp/Signal/
Slack do it.

The fetch is the node's, never the browser's or the hub's. The browser
cannot: a strict img-src/connect-src and CORS block it, and a direct fetch
would leak every reader's IP to the linked host on each render. The hub must
not touch group content (draft-v6 §2.5). The node already fetches third-party
metadata for the Videos and Music apps, over the same authorised path.

Flow mirrors media_meta_req: the client sends `link_preview_req {url}`, the
node replies `link_preview_resp` with the card fields (or `ok: false`), and
any OG image is stored under its blake3 in the existing media_cache thumb
store — the client then fetches it via the normal file_req path, exactly like
a poster. Nothing durable is added: the card text lives in a bounded in-memory
TTL cache on the node (draft-v6 §2.7 — enrichment on demand, the asking device
caches), and MNP goes 0.11 → 0.12 (additive: an older node logs "unknown type"
and the client shows the bare link).

Because the URL is chosen by a *member* and triggers an outbound request from
the operator's machine, `linkpreview.safe_url` is an SSRF gate: http(s) only,
no credentials, and every resolved address must be globally routable — no
loopback, private, link-local, multicast or reserved range, cloud-metadata
included. Redirects are followed by hand so each hop is re-checked. Residual,
documented in the module: DNS rebinding between the check and connect, closed
properly by pinning the checked IP — a follow-up.

Also fixes a long-standing chat annoyance the preview cards made worse:
opening the Chat tab landed a screen or two above the newest message because
the scroll-to-bottom ran before attachment thumbnails and (now) preview cards
had loaded and grown the content. A ResizeObserver keeps the view pinned to
the bottom through late content growth, and does nothing once the reader
scrolls up.

Tests: test_linkpreview.py (the SSRF gate and the OpenGraph parse, incl.
redirect re-validation and image downscaling) and test_link_preview_request.py
(reply shape, the media_cache image round-trip, the result cache).

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018gKJ85aZyvEwarXMFzFEwi
</content>
</entry>
<entry>
<title>feat(hub): split the group UI into a pluggable "applications" architecture</title>
<updated>2026-08-23T13:15:35Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-23T13:15:35Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=9f02ee2c09652abf1308bdfa4a3eec4e9ca9ac83'/>
<id>urn:sha1:9f02ee2c09652abf1308bdfa4a3eec4e9ca9ac83</id>
<content type='text'>
GroupPage's 6620-line app.js carried Chat and Files wedged in directly, with
no way to add another group-level app without touching the shell itself. It
is now app.js (routing, non-group pages) plus nine focused files — apps.js
(the registry), chat-app.js, files-app.js, video-player.js, group-page.js
(the shell), group-settings.js, hub-client.js, icon.js and file-utils.js —
with docs/apps.md as the checklist for adding one (Videos/Music/Photos are
sketched there, not built).

Node side gained the matching enablement mechanism, mirroring
member_upload exactly: a roster setting, a signed apps_enabled op enforced
by _has_admin_authority, exposed in the handshake ack. Operators toggle
applications per group from Settings, which also gained a small reorder:
Invite, Pairing, Applications, Shared directories, Uploads, danger zone,
Your devices, Members.

Two bugs surfaced during the split, both missing an import across the new
file boundary and invisible to node --check or a module-load probe since
they only throw when the code path actually runs:

- group-page.js called onRefreshAuth on a stale-token handshake rejection,
  but app.js never imported refreshAccessToken from hub-client.js — so a
  brand new member (including a group's own creator) hit "Not a member of
  this group" and the retry silently failed, throwing before it could
  refresh the token.
- chat-app.js called getLocale() for message timestamps without importing
  it from i18n.js. Opening Chat on a group with real messages threw mid-
  render; uncaught, that appears to wedge Preact's render scheduler, so
  every button on the page stopped responding until reload.

Caught the second class of bug with a proper no-undef audit across all
split files (a temporarily installed ESLint 9, since the system one is too
old to parse this codebase's syntax) rather than trusting grep. 827 tests
pass; 6 new ones cover the apps_enabled policy.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_016SF6RKNBKg9qejmoMJ9ybA
</content>
</entry>
</feed>
