From 1d2d394428944b3255267e8a97ca846db2fbc547 Mon Sep 17 00:00:00 2001 From: Day Dreamer <87640074+HugoXK@users.noreply.github.com> Date: Tue, 3 Nov 2026 07:36:33 +0800 Subject: [PATCH 1/2] social: draw no room-overview write control in a read-only window FaceRoomOverview still drew Leave, the Auto-send switch and the invite in a read-only window, and each of them can only be refused by the host as bot/read-only. The component already had the chip-only head for a past room; it now draws the same head when roster.readOnly is set, and a waiting room keeps its wait code and roster tiles but drops Leave and Enter chat. Fixes #2228 --- changelog.d/2228-room-overview-readonly.fixed.md | 1 + docs/subsystems/social.md | 2 +- plugins/social/client.js | 10 +++++++++- 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 changelog.d/2228-room-overview-readonly.fixed.md diff --git a/changelog.d/2228-room-overview-readonly.fixed.md b/changelog.d/2228-room-overview-readonly.fixed.md new file mode 100644 index 000000000..887efbcf0 --- /dev/null +++ b/changelog.d/2228-room-overview-readonly.fixed.md @@ -0,0 +1 @@ +A read-only FlyBot window no longer draws the Bot Social room overview's Leave, Auto-send switch or invite: the room view keeps its status chip (and a waiting room keeps its wait code and roster) and offers no write control, while a writable window keeps the same controls and the two-tap Leave ([#2228](https://github.com/ThinkFlowLab/FlyBot/issues/2228)). diff --git a/docs/subsystems/social.md b/docs/subsystems/social.md index 671b95929..d6e083eb7 100644 --- a/docs/subsystems/social.md +++ b/docs/subsystems/social.md @@ -195,7 +195,7 @@ Settings → **Bot Social** lists friends and shares once the server is connecte ### Past rooms, unique titles, poll, and + (#129–#133) - Expired matching (`candidate` whose `expiresAt` has passed) and a peer leaving the last remote seat move the hosted room to **Past rooms** on the next poll (`rememberPastFace`, status `closed` / `left`). The local room stays so **Open** still works; **Leave** is not shown on those cards. Face-room names are `Face-to-face · Alice, Bob · 482913` (people plus the 6-digit code, or `Oct 16 21:40` when there is no code) so two rooms with the same people stay distinct. The literal `Face-to-face room` is not a fallback. The number printed there and on the waiting card is the six digits only (#1005): `faceWaitCode` and `shownFaceTitle` refuse a stored value that is not exactly six digits (`1234567` never reads as `123456`) and fall back to the code the owner typed, or to the creation time. -- Ended-room state and titles (#304): when every other home's Bots have left an open room, this home marks it **Closed** (it did not leave; `faceSnapshotPast` → `closed` with `peersGone`) and then leaves it on Bot Social itself (`leaveFaceRoom({ status: 'closed' })`), so it stops polling the room and the room-only identity is released (invariant 4). If that leave fails (timeout, 5xx), the next poll that still finds the room open on Bot Social tries it again. A room that drops off Bot Social's list without this home leaving is the same end (#1070): the hosted room is marked `closed` so the sidebar's active list drops it, and Past rooms keeps that one row (a `left` room stays Left). Its `endedAt` is the snapshot's `closed_at` when Bot Social sent one, so both homes show that time; otherwise the first time this home noticed. The owner who left keeps **Left**: `rememberPastFace` never turns a `left` row into `closed` when a later poll finds the room gone, and their room reads `You left this room.` while the others read ` left the room. Room closed.` That status line is the one leave signal when the room closes (#1103): the shorter ` left the room` is not also posted into the transcript. The number a room was joined by goes with every `upsertFace` (`joinCode`) and core keeps it on `room.face.joinCode`, so the title stays `Face-to-face · Alice, Bob · 482913` through polls, Leave and the end; Past rooms show that same title (people rebuilt from the row's members when it has them, the number or stored time kept; with neither, the creation time, never the joining deadline). A left or closed room's details are read-only: the status chip only, no Auto-send and no Leave. Past rows list everyone who was in the room (#304): `faceRoomTitle` takes the snapshot's members plus every participant the hosted room ever had (like the core title), and `viewHostedFace` for a closed or left room lists all participants, marking who had left `left: true` (Open on a dropped room seats only the others again); a row stored before this is refreshed from its hosted room. Each row shows its time (`data-flybot=face-time`): `endedAt`, set once by `rememberPastFace`, else the start (or creation) time; today reads `14:05`, an earlier day `Oct 19`, and the hover text has the full `Ended Oct 19 14:05`. The fold remembers whether it is open across a reload (`localStorage` `flybot.social.pastRoomsOpen`). A read-only window (the shell names it in `roster.readOnly`) keeps every read and drops every write (#2219): the section still polls `social/status` and still lists share requests, accept notices and past rooms, but draws no Dismiss, no Ask them to share a Bot, no Leave and no decision on a held send, offers none of the three Bot Social rows in the sidebar `+` menu, and `social/face-open` returns the stored past view (`readOnly: true`, name, status, members) instead of `upsertFace`-ing a local mirror, so reopening a past room creates no room; a writable window behaves exactly as before. +- Ended-room state and titles (#304): when every other home's Bots have left an open room, this home marks it **Closed** (it did not leave; `faceSnapshotPast` → `closed` with `peersGone`) and then leaves it on Bot Social itself (`leaveFaceRoom({ status: 'closed' })`), so it stops polling the room and the room-only identity is released (invariant 4). If that leave fails (timeout, 5xx), the next poll that still finds the room open on Bot Social tries it again. A room that drops off Bot Social's list without this home leaving is the same end (#1070): the hosted room is marked `closed` so the sidebar's active list drops it, and Past rooms keeps that one row (a `left` room stays Left). Its `endedAt` is the snapshot's `closed_at` when Bot Social sent one, so both homes show that time; otherwise the first time this home noticed. The owner who left keeps **Left**: `rememberPastFace` never turns a `left` row into `closed` when a later poll finds the room gone, and their room reads `You left this room.` while the others read ` left the room. Room closed.` That status line is the one leave signal when the room closes (#1103): the shorter ` left the room` is not also posted into the transcript. The number a room was joined by goes with every `upsertFace` (`joinCode`) and core keeps it on `room.face.joinCode`, so the title stays `Face-to-face · Alice, Bob · 482913` through polls, Leave and the end; Past rooms show that same title (people rebuilt from the row's members when it has them, the number or stored time kept; with neither, the creation time, never the joining deadline). A left or closed room's details are read-only: the status chip only, no Auto-send and no Leave. Past rows list everyone who was in the room (#304): `faceRoomTitle` takes the snapshot's members plus every participant the hosted room ever had (like the core title), and `viewHostedFace` for a closed or left room lists all participants, marking who had left `left: true` (Open on a dropped room seats only the others again); a row stored before this is refreshed from its hosted room. Each row shows its time (`data-flybot=face-time`): `endedAt`, set once by `rememberPastFace`, else the start (or creation) time; today reads `14:05`, an earlier day `Oct 19`, and the hover text has the full `Ended Oct 19 14:05`. The fold remembers whether it is open across a reload (`localStorage` `flybot.social.pastRoomsOpen`). A read-only window (the shell names it in `roster.readOnly`) keeps every read and drops every write (#2219): the section still polls `social/status` and still lists share requests, accept notices and past rooms, but draws no Dismiss, no Ask them to share a Bot, no Leave and no decision on a held send, offers none of the three Bot Social rows in the sidebar `+` menu, and `social/face-open` returns the stored past view (`readOnly: true`, name, status, members) instead of `upsertFace`-ing a local mirror, so reopening a past room creates no room; the room view's own head row draws the same chip-only head as a past room, with no Leave, no Auto-send switch and no invite (#2228), while a waiting room keeps its wait code and roster and drops its Leave and Enter chat; a writable window behaves exactly as before. - The face-to-face join field for the room number starts empty with a light `6 digits` hint (normal letter spacing, 13px), not a `000000` that looks pre-filled (#173). A number that is not empty and not exactly six digits keeps **Join** disabled and shows **Enter 6 digits** under the field (#1052). Six digits clear that line and enable Join once a Bot is picked; an empty field shows nothing. - Round-3 polish (#173): **Accept invite** is no longer a separate **+** row; **Add friend** opens the single join panel for both a handle and an invite link or code. Escape closes the Add friend and Face-to-face panels, the `000000` code is a placeholder, Past rooms and friend rows use shorter copy, and Bot settings group invite and access under **Sharing** / **People with access**. - **Open** on a Past room calls `social/face-open` (`plugins/social/src/social.js`): it returns the existing local hosted session or recreates one from the saved room, so a peer home can open a room that already left Bot Social (#171). Titles come from `plugins/social/src/face-title.js`, with participant owners in stable handle order, so both homes show the same name (#172). diff --git a/plugins/social/client.js b/plugins/social/client.js index a707188c9..8034831f0 100644 --- a/plugins/social/client.js +++ b/plugins/social/client.js @@ -2823,6 +2823,9 @@ window.__ModuleLoader__.load({ ?? (room.presentedParticipants ?? room.participants ?? []).filter(p => p.leftSeq === undefined) const expiresAt = snapshot?.expiresAt || face?.expiresAt const mineMember = member => member.remote === false || Boolean(member.id && roster?.byId?.[member.id]) + // #2228: a read-only window draws the same chip-only head as a past room — Leave, the Auto-send + // switch and the invite can only be refused by the host there. + const locked = readOnlyReason(roster) !== '' if (candidate) { // The open dialog already shows this waiting room: one card, not two (#306). if (room.id && faceDialog.roomId === room.id) return null @@ -2844,7 +2847,7 @@ window.__ModuleLoader__.load({ : faceLook({ roster, member })), }))), othersIn(members, mineMember) ? null : h('div', { className: 'fs-hint', 'data-flybot': 'face-wait-alone' }, 'Waiting for others to join…'), - h('div', { className: 'fs-wait-foot', 'data-flybot': 'face-wait-foot' }, + locked ? null : h('div', { className: 'fs-wait-foot', 'data-flybot': 'face-wait-foot' }, h(ConfirmLeave, { busy, size: 'fs-btn', @@ -2876,6 +2879,11 @@ window.__ModuleLoader__.load({ return h('div', { className: 'fs-face-head', 'data-flybot': 'face-overview', 'data-past': true }, h('span', { className: 'fs-chip fs-chip-status', 'data-flybot': 'face-status', 'data-status': faceChip(statusRoom) }, faceChipLabel(statusRoom))) } + if (locked) { + // The window is read-only (#2228): the same chip-only head as a past room. + return h('div', { className: 'fs-face-head', 'data-flybot': 'face-overview', 'data-readonly': true }, + h('span', { className: 'fs-chip fs-chip-status', 'data-flybot': 'face-status', 'data-status': faceChip(statusRoom) }, faceChipLabel(statusRoom))) + } // Leave shares the status line, a row away from the Auto-send toggle, so a tap meant for one // never hits the other (#306). return h('div', { className: 'fs-face-head', 'data-flybot': 'face-overview' }, From fc4687d2674dc5cce807e7d7c4d0334604796dbb Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Tue, 3 Nov 2026 08:14:57 +0800 Subject: [PATCH 2/2] test(social): bind the read-only room-overview head (#2228) The merge gate asks the fix to carry the assertion it changes: mount FaceRoomOverview with a read-only roster and a live origin:"face-to-face" room and require zero Leave / Auto-send / invite tags, keep the writable window s five tags and two buttons, and keep a waiting room s code and roster while dropping its Leave and Enter chat foot. Red first on 986399246f: two of the three #2228 tests fail there; plugins/social is 566/566 at this commit. Fixes #2228 Owner: Dev-4 Written-by: Rework Tier: T2 Branch: dev/4-p443-room-overview-readonly Base: 986399246ff8c26bd418978785ce1199dbd239ca Parent: 1d2d394428944b3255267e8a97ca846db2fbc547 --- plugins/social/test/social-readonly.test.js | 54 +++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/plugins/social/test/social-readonly.test.js b/plugins/social/test/social-readonly.test.js index cfe9edde7..91a890217 100644 --- a/plugins/social/test/social-readonly.test.js +++ b/plugins/social/test/social-readonly.test.js @@ -226,3 +226,57 @@ test('#2219: a writable window keeps the decision buttons', async () => { assert.equal(byTag(tree, 'hold-always').length, 1, 'Auto-send from now on stays') } finally { delete globalThis.__flybot__ } }) + +// #2228: the room view's own head row. A live room's overview drew Leave, the Auto-send switch and +// the invite in a read-only window, and the three host endpoints behind them can only be refused. +const overviewRoom = () => ({ + id: 'r1', + origin: 'face-to-face', + face: { roomId: 'frm1', status: 'open', joinCode: '482913' }, + participants: [{ id: 'bot_a', name: 'Alice', remote: false, handle: '@alice.home' }], +}) +const WRITE_TAGS = ['face-leave', 'face-auto-send', 'face-auto-bot', 'face-invite', 'face-invite-mint'] +const overview = (roster, room = overviewRoom()) => { + const inst = { hooks: {}, effects: [], props: { room, roster, actions: {} } } + return render(loadApp().FaceRoomOverview, inst) +} + +test('#2228: a read-only window draws no room-overview write control', async () => { + fixtures() + try { + const tree = overview(readOnlyRoster()) + const tags = flybotTags(tree) + // Not hidden: not drawn at all. Red first on 986399246f: 5 tags and Leave / Create link. + const writes = tags.filter(tag => WRITE_TAGS.includes(tag)) + assert.deepEqual(writes, [], `no write control in a read-only room view, saw ${writes.join(',')}`) + const writeButtons = buttonLabels(tree).filter(label => label === 'Leave' || label === 'Create link') + assert.deepEqual(writeButtons, [], `no write button in a read-only room view, saw ${writeButtons.join(',')}`) + // The read half stays: the head is the status chip, not an empty row. + assert.ok(tags.includes('face-status'), 'the status chip is still drawn') + } finally { delete globalThis.__flybot__ } +}) + +test('#2228: a writable window keeps the three room-overview controls', async () => { + fixtures() + try { + const tree = overview(writableRoster()) + const tags = flybotTags(tree) + for (const tag of WRITE_TAGS) assert.ok(tags.includes(tag), `a writable window keeps ${tag}`) + const labels = buttonLabels(tree) + assert.ok(labels.includes('Leave'), 'a writable window keeps Leave') + assert.ok(labels.includes('Create link'), 'a writable window keeps Create link') + } finally { delete globalThis.__flybot__ } +}) + +test('#2228: a read-only waiting room keeps its code, drops Leave and Enter chat', async () => { + fixtures() + try { + const room = { ...overviewRoom(), face: { roomId: 'frm1', status: 'candidate', joinCode: '482913' } } + const tree = overview(readOnlyRoster(), room) + const tags = flybotTags(tree) + assert.deepEqual(byTag(tree, 'face-wait-foot'), [], 'no Leave / Enter chat foot in a read-only window') + assert.deepEqual(buttonLabels(tree).filter(label => label === 'Leave' || label === 'Enter chat'), []) + assert.ok(tags.includes('face-wait-code'), 'the room number is still drawn') + assert.ok(tags.includes('face-wait-roster'), 'who has joined is still drawn') + } finally { delete globalThis.__flybot__ } +})