diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-18 17:46:54 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-18 17:46:54 +0200 |
| commit | cd2e89f5f5cccdb116db4fcb82d00b6325972782 (patch) | |
| tree | 17be58fb00b49736f818a2f8063464960b5b1101 /packages/meshbay-hub/src/meshbay_hub/static/app.js | |
| parent | 2ef5498ab1509a93691b87b4cc5d9b52bb3f52dc (diff) | |
| download | meshbay-cd2e89f5f5cccdb116db4fcb82d00b6325972782.tar.gz | |
fix: the chat tab no longer scrolls, and a group is listed or invite-only
**The chat tab was 8px too tall, at every window size.** The panel is sized from
JS to `viewport - top - 16`, which puts its bottom 16px above the fold — but it
sits inside `.main`, which adds 24px of padding below it. Eight pixels of
document past the window, whatever the window. Measured at 700, 900 and 1200:
`scrollHeight` 708, 908, 1208.
This is the second one of these — the sign-in card was `.page-center` and
`.layout` each reserving `100vh - 52px` — so it is now measured in the suite
rather than reasoned about. `tests/harness/scroll_probe.py` renders the real
markup against the real stylesheet and **runs the real `fit()` lifted out of
`app.js`**: a copy of the formula in a test would go on passing after the
original changed, which is exactly the bug being guarded.
The fix does not encode 24 anywhere. The first pass runs as before, then the
leftover is measured and taken off, so anything added below the panel later is
absorbed the same way. Now `scrollHeight == innerHeight` at all three heights,
nothing below the fold, and the panel still fills the room it has — that last
one has its own test, because shrinking the chat to 240px would satisfy every
other assertion here and be useless.
The Settings tab was measured too and is **not** a bug: it fits at 1200px and
overflows only when its content is genuinely taller than the window.
**Group creation asked one question twice.** Visibility and admission were
separate selectors that could only ever be set together — picking Public reached
over and set the policy — and two of the four combinations are meaningless. The
API already refused public+invite with a 422, so the form could build a request
that could not succeed. Private+open was accepted and should not have been: a
group anyone may join that nobody can find is a listing with the listing
removed, since joining goes through the node and there is no link to pass
around.
So: one selector, "who can join", and the request derives the rest. The API now
refuses the other impossible pair as well, with a message that says which way to
resolve it. Six locale strings the visibility box owned are deleted rather than
left unread in ten files, and the two surviving descriptions now say what each
choice means for who can *find* the group — with the word "public" gone from the
page, nothing else would have said it, and someone would publish a group without
meaning to.
865 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/app.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/app.js | 80 |
1 files changed, 37 insertions, 43 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index eb11469..62bbfa5 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -1005,7 +1005,6 @@ function ExplorePage({ token, myGroupIds }) { function CreateGroupPage({ token, onCreated }) { const [name, setName] = useState(''); const [description, setDescription] = useState(''); - const [visibility, setVisibility] = useState('private'); const [joinPolicy, setJoinPolicy] = useState('invite'); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); @@ -1016,7 +1015,10 @@ function CreateGroupPage({ token, onCreated }) { setLoading(true); setError(''); try { - const body = { name: name.trim(), visibility, join_policy: joinPolicy }; + // Derived, not asked: "open" is what makes a group listed, and there is + // no third combination the server would accept. + const body = { name: name.trim(), join_policy: joinPolicy, + visibility: joinPolicy === 'open' ? 'public' : 'private' }; if (description.trim()) body.description = description.trim().slice(0, 512); const data = await hubFetch('/v1/groups', { method: 'POST', token, body, @@ -1055,59 +1057,39 @@ function CreateGroupPage({ token, onCreated }) { </div> </div> + ${/* One question, not two. Visibility and admission were separate + selectors that could only ever be set together: a public group + admits everyone by definition, and a private one that anyone may + join is a directory listing nobody can find. The server already + refused public+invite with a 422 — the form could build a request + that could not succeed. Now the answer to "who can join" settles + both, and the descriptions say what each one means for who can + *find* the group, which is the part the visibility box was there + to state and no longer needs to. */ html` <div class="settings-section"> - <h3 class="settings-heading">${t('create_group.visibility')}</h3> + <h3 class="settings-heading">${t('create_group.join_policy')}</h3> <div class="choice-list"> - <label class="choice ${visibility === 'private' ? 'selected' : ''}"> - <input type="radio" name="visibility" checked=${visibility === 'private'} - onChange=${() => { setVisibility('private'); setJoinPolicy('invite'); }} /> + <label class="choice ${joinPolicy === 'invite' ? 'selected' : ''}"> + <input type="radio" name="join_policy" checked=${joinPolicy === 'invite'} + onChange=${() => setJoinPolicy('invite')} /> <${Icon} name="lock" cls="choice-icon" /> <span class="choice-text"> - <span class="choice-title">${t('create_group.private')}</span> - <span class="choice-desc">${t('create_group.private_desc')}</span> + <span class="choice-title">${t('create_group.invite')}</span> + <span class="choice-desc">${t('create_group.invite_desc')}</span> </span> </label> - <label class="choice ${visibility === 'public' ? 'selected' : ''}"> - <input type="radio" name="visibility" checked=${visibility === 'public'} - onChange=${() => { setVisibility('public'); setJoinPolicy('open'); }} /> + <label class="choice ${joinPolicy === 'open' ? 'selected' : ''}"> + <input type="radio" name="join_policy" checked=${joinPolicy === 'open'} + onChange=${() => setJoinPolicy('open')} /> <${Icon} name="globe" cls="choice-icon" /> <span class="choice-text"> - <span class="choice-title">${t('create_group.public')}</span> - <span class="choice-desc">${t('create_group.public_desc')}</span> + <span class="choice-title">${t('create_group.open')}</span> + <span class="choice-desc">${t('create_group.open_desc')}</span> </span> </label> </div> - - ${visibility === 'public' - ? html`<p class="settings-hint" style="margin-top:12px"> - ${t('create_group.public_is_open')} - </p>` - : html` - <h3 class="settings-heading" style="margin-top:20px"> - ${t('create_group.join_policy')} - </h3> - <div class="choice-list"> - <label class="choice ${joinPolicy === 'invite' ? 'selected' : ''}"> - <input type="radio" name="join_policy" checked=${joinPolicy === 'invite'} - onChange=${() => setJoinPolicy('invite')} /> - <${Icon} name="envelope" cls="choice-icon" /> - <span class="choice-text"> - <span class="choice-title">${t('create_group.invite')}</span> - <span class="choice-desc">${t('create_group.invite_desc')}</span> - </span> - </label> - <label class="choice ${joinPolicy === 'open' ? 'selected' : ''}"> - <input type="radio" name="join_policy" checked=${joinPolicy === 'open'} - onChange=${() => setJoinPolicy('open')} /> - <${Icon} name="door" cls="choice-icon" /> - <span class="choice-text"> - <span class="choice-title">${t('create_group.open')}</span> - <span class="choice-desc">${t('create_group.open_desc')}</span> - </span> - </label> - </div> - `} </div> + `} <button class="btn-primary" type="submit" disabled=${loading}> ${loading ? t('create_group.creating') : t('create_group.submit')} @@ -2875,6 +2857,18 @@ function ChatPanel({ transportRef, username, entries, gekRef, onRefreshIndex, on // the result — the answer must be the same either way. const top = el.getBoundingClientRect().top + window.scrollY; el.style.height = `${Math.max(CHAT_MIN_HEIGHT, vh - top - CHAT_BOTTOM_GAP)}px`; + // What sits *below* the panel is not knowable from up here — today it is + // `.main`'s 24px bottom padding against this 16px gap, which left the + // document 8px taller than the window and a scrollbar on the chat tab at + // every window size. Rather than encode 24 somewhere and have the next + // change to the page break it again, the leftover is measured and taken + // off. Self-correcting: anything added under the panel is absorbed the + // same way. + const over = document.documentElement.scrollHeight - vh; + if (over > 0) { + el.style.height = + `${Math.max(CHAT_MIN_HEIGHT, el.getBoundingClientRect().height - over)}px`; + } }; fit(); window.addEventListener('resize', fit); |