diff --git a/changelog.d/1052-enter-6-digits.fixed.md b/changelog.d/1052-enter-6-digits.fixed.md new file mode 100644 index 000000000..cd0f6f48b --- /dev/null +++ b/changelog.d/1052-enter-6-digits.fixed.md @@ -0,0 +1 @@ +Typing a room number that is not exactly six digits keeps Join disabled and shows "Enter 6 digits" under the field (Chinese: 请输入 6 位数字). An empty field stays quiet, and six digits still join ([#1052](https://github.com/ThinkFlowLab/FlyBot/issues/1052)). diff --git a/core/client.js b/core/client.js index fa346d38d..3ebc93858 100644 --- a/core/client.js +++ b/core/client.js @@ -66,6 +66,8 @@ var ZH = { "6-digit room number": "6 位房间号", "Pick a Bot first.": "请先选一个 Bot。", "Enter all 6 digits.": "请输入完整的 6 位数字。", + // #1052: visible under the field when the number is not empty and not six digits. + "Enter 6 digits": "请输入 6 位数字", "Join with link": "用链接加入", "Scan a code": "扫码", "Join group": "加入群", diff --git a/core/src/client/i18n.js b/core/src/client/i18n.js index df96f30f9..fbeb16b4c 100644 --- a/core/src/client/i18n.js +++ b/core/src/client/i18n.js @@ -29,6 +29,8 @@ const ZH = { '6-digit room number': '6 位房间号', 'Pick a Bot first.': '请先选一个 Bot。', 'Enter all 6 digits.': '请输入完整的 6 位数字。', + // #1052: visible under the field when the number is not empty and not six digits. + 'Enter 6 digits': '请输入 6 位数字', 'Join with link': '用链接加入', 'Scan a code': '扫码', 'Join group': '加入群', diff --git a/docs/subsystems/core-client.md b/docs/subsystems/core-client.md index 64b2e192a..ea7384035 100644 --- a/docs/subsystems/core-client.md +++ b/docs/subsystems/core-client.md @@ -73,7 +73,7 @@ The overlay (`flybot-slots.js` `FlyBotApprovals`) is the fallback, not a second ### Face-to-face room lead -- A local group's **Members · N** card (`group-settings.js`) ends with the plugin rows of slot `flybot.room.members` (`FlyBotRoomMembers` in `flybot-slots.js`), after the note on who leads the group (#892) and on the card's own row padding (`.bt-flybot-room-members`), never in a face or shared (`inbound-share`) room; the rows get `host`, the core endpoint call (also `__flybot__.host`), for `room-invite` / `room-invite-mint` / `room-invite-revoke` (#876, the social plugin's **Invite by link**). Covered by `core/test/room-members-slot.test.js`. Plugin copy that core's dictionary carries is translated through `__flybot__.t` (#892: Invite by link, the Join-or-create dialog), so a zh UI reads it in Chinese. A room a link opened is titled by its own name on the joining home (`roomTitle` in `remote-label.js`: sidebar, header, drawer, brand); "Shared room from …" stays as the sidebar row's hover and the **Shared** tag's title (#892). In a room projected from another home the Members rows carry what that side could confirm (#904): a Bot learned from the host's roster reads **Unconfirmed** until this home holds its own fact about it, one with no Bot Social identity reads **Not connected**, and the remove **×** is not offered there — the host's membership is changed on the host's side. +- A local group's **Members · N** card (`group-settings.js`) ends with the plugin rows of slot `flybot.room.members` (`FlyBotRoomMembers` in `flybot-slots.js`), after the note on who leads the group (#892) and on the card's own row padding (`.bt-flybot-room-members`), never in a face or shared (`inbound-share`) room; the rows get `host`, the core endpoint call (also `__flybot__.host`), for `room-invite` / `room-invite-mint` / `room-invite-revoke` (#876, the social plugin's **Invite by link**). Covered by `core/test/room-members-slot.test.js`. Plugin copy that core's dictionary carries is translated through `__flybot__.t` (#892: Invite by link, the Join-or-create dialog, including **Enter 6 digits** under a partial room number, #1052), so a zh UI reads it in Chinese. A room a link opened is titled by its own name on the joining home (`roomTitle` in `remote-label.js`: sidebar, header, drawer, brand); "Shared room from …" stays as the sidebar row's hover and the **Shared** tag's title (#892). In a room projected from another home the Members rows carry what that side could confirm (#904): a Bot learned from the host's roster reads **Unconfirmed** until this home holds its own fact about it, one with no Bot Social identity reads **Not connected**, and the remove **×** is not offered there — the host's membership is changed on the host's side. - The face room's details (`FaceMembers` in `details-drawer.js`) open with `FaceLead` (`group-settings.js`, #476): the current lead as `Bot (person)`, *Default* until the creator picked one. Only when `room.face.lead.canPick` (this home made the room and Bot Social stores a lead) does it show **Change**, a **Make lead** button per Bot of every home and **Use the default lead**; each calls `social/face-lead`. Everyone else sees it read-only. `FacePersonRow` marks the lead's Bot with a *Lead* tag (`isLeadSeat`, by handle). Never a chat command. ### Room avatars in face-to-face chats diff --git a/docs/subsystems/social.md b/docs/subsystems/social.md index 9b10da360..bf802ee9f 100644 --- a/docs/subsystems/social.md +++ b/docs/subsystems/social.md @@ -47,7 +47,7 @@ Everything in `plugins/social/` (entry `plugins/social/index.js` and any helpers 3. A server policy pause (`conversation_paused`: loop suspected between the Bots, emergency stop, or an older server's turn budget) never surfaces as a bare send error: both send paths (the `bot-social` transport and the remote-chat sender) map it to *"Bot Social paused that chat (reason). Send your message again: it starts a new chat."* (structured: `code: 'conversation_paused'`, `paused`, `resend`), because `send_as_member` opens a fresh conversation for the pair's next message. It never points to the Bot Social website (`/ui`), which a FlyBot owner cannot sign in to, and never names Extend or turns (there is no turn limit, owner decision 2026-10-19, Bot Social #28; see below) and no other member Bot is tried for it. Other failures end in *Try again.*, never in a console pointer. Other `sendChat` failures are one short English line with a next step (reconnect, invite, try again) — never `Bot Social send_as_member: …`. A human's own message in a face-to-face or shared room to a friend's Bot that is already a member of that room is delivered as that human in the room (`onFaceLine` / room log); it is not signed or grant-checked as Chief (#143). Chief-originated `bot.send` / `message_friend_bot` to an ungranted friend Bot stays denied. The refusal says it the right way round and names the one setting that exists (#273): the sending Bot is not shared with that friend (*"Chief is not shared with that friend, so it cannot message their Bots. To allow it, share Chief with them in its settings → Sharing."*), or the target is not a Bot a friend shared with you; `message_friend_bot` tells the model to relay that reason as written. 4. Access is a per-Bot **grant** (#90, #138): default deny, Chief included. `register` / `registerAll` register a Bot when it is invited, when it joins an invite, when it already has a person-grant or a saved identity, or at face-to-face join (`forFace`, `roomOnly`). `pollOnce` reads inboxes of non-room-only members only. Friends' Bots is `list_remote_bots` (Bots granted **to me**), minus reverse-friends of invites I created and minus face-room-only remotes. Accepting **Sharing → Copy invite link** is the grant the other way; **Share with a friend** (`social/share-friends` + `social/share-with-friend`, via `grantBotToFriend` / `grant_bot_to_friend`) is the same grant for someone who is already a friend, with no invite code; both endpoints register the Bot's Bot Social identity on demand (`register(bot, { forInvite: true })`) when it has none yet, so the share list works on a Bot that was never invited or shared. Joining does not grant my talk-through Bot back. Join offers **Talk through** only when I have more than one Bot (default Chief; tooltip: not shared unless I invite them to it, #228). `social/access` + `social/revoke-grant` (via `mcp-client.js` `listGrants` / `revokeGrant`, preferring `list_bot_grants` / `revoke_bot_grant`) list people as `Name (@home)` and revoke one person. Core `reach()` / `flybot.grants` are the unbypassable gate. **Face-to-face join (#83 / #89):** bringing my own Bot by number is an implicit room-scoped grant. If it has no identity, join registers it room-only; friends' `list_remote_bots`, invites, polling and `reach()` outside the room stay denied. When that Bot leaves its last face room or the room closes, the plugin revokes the room-only identity (`remove_home_member`) and drops room-scoped remotes from Friends' Bots. A failed join that created a new room-only identity is revoked too. A later person-grant reuses the handle and drops `roomOnly`, keeping the stored `ownerName`/`home` and filling either from `whoami` when the room-only record never had it (#863). Registration also records the owner-level `ownerName` and `home` on that member identity (#848) from one `whoami`: top-level `display_name` and `home`, else `connector_identity.bot_handles[0]`, else the `flybot-home` entry in `bots[]` (given the `@`), and `registerAll` fills them into an identity saved before this without registering it again; the group room-share envelope reads them to name the inviter. 5. First start retries a taken home handle (`FlyBot`, `FlyBot2`, …) instead of failing for a YAML edit (#136). If every retry is still taken, the social UI never shows the host's `Unknown FlyBot endpoint social/…` message: `apply` registers the social UI endpoints with a friendly `Social is not running: ` refusal and `flybot.warn` records the failure. The client also maps `bot/not-found` on `social/*` to that idle state. Add friend shows `You are @your.home` (#181). -6. Face-to-face join (#83, #92, #158) is a waiting room, not an instant chat: the owner picks local Bots, types the 6-digit number, then waits with the same code shown large until someone **Enter chat**s. Copy never says there is no preview. Each picker tile and each waiting-room member uses the host Bot avatar (`__flybot__.BotAvatar`), not a shared first-letter badge. The client calls `social/face-join` once per Bot with the same code and shows partial failures. If a chosen Bot has no identity, the plugin registers it first (see invariant 4); registration failures (`handle_taken`, rate limit, timeout) map to English copy and leave no leftover member. Failed join responses never leak `members` / `events` / `room_id`. Every FACE_ROOMS.md error code maps to English copy (`rate_limited` / `locked_out` include `retry_after`). The waiting room polls `social/face-rooms` (which refreshes from Bot Social, including `joinCode`) and draws one tile per person (#457: `FacePersonTile`, the person's name, `Me (you)` for the owner, with the Bots they brought underneath; the owner's tile has a tiny accent dot); **Leave** and **Enter chat** share the footer. **Enter chat** is `social/face-start` then `openSession(roomId)`, and the screen follows if someone else starts. Peer `room.message` text is untrusted: strip fence markers and wrap in `` before anything model-visible. Bot replies POST `/v1/face-rooms/{id}/messages`; `pending_approval` shows in the existing queue and in the chat (owner Approve / Reject). Owner-typed `FLYBOT-FACE-HUMAN` lines stay on that card until Approve / Always approve / Reject unless face **Auto-send** is on; the card and chat never show the raw envelope (#159). Face-room **Auto-send** is one switch per Bot of mine in the room (#457, `Auto-send Chief → room`, `social/face-auto { botId, on }`, kept in this home's `settings.faceAutoSend`, off by default): when that Bot's line comes back `pending_approval` and Bot Social says the hold is its own blanket rule (`hold_reason: 'blanket_outbound'`) or nothing but the room's Auto-send switch (`hold_reason: 'face_room_auto_send_off'` — what a clean face-room line carries once that switch is the only thing left), the host approves it at once (`decide_approval`; the switch is the owner's standing consent, so a switch that reads **on** has to send), so the room sees it; any other `hold_reason` (`risk`, `attachment`, `policy`, a value this build does not know) and other Bots' lines still wait for the owner. Only this face-room path accepts `face_room_auto_send_off` (`holdAllowsFaceAutoSend`, on top of `holdIsBlanket`); the two outbound paths keep the blanket rule alone (#865). The older account-wide `auto_approve_face_rooms` (`whoami` / MCP `set_auto_approve`) reads as every switch on; the first per-Bot change keeps the others on here and turns the account-wide one off (a `confirmation_required` answer is retried with `confirm_code` in the same click; the UI never asks for a code). REST `/v1/me` is not used with the connector token. A friend's Bot in the room member list and waiting grid uses the host Bot avatar (handle-seeded when Bot Social sends no image). New members only receive events from their join point (`after` cursor; the server also floors it). Join does not grant the Bot to friends. The UI never says published. A number nobody else is using opens no room (#458): `social/face-join` without `start` answers `{ nobody: true, faceRoomId }` when the server's room holds only this home's Bots, and the host keeps that fresh candidate off the sidebar and Past rooms (an in-memory hold, dropped on the next poll after 2 minutes). The dialog shows **Nobody is waiting under …** with **Cancel** (`social/face-leave { faceRoomId }` leaves it on Bot Social and releases a new room-only identity) and **Start a new room** (`start: true` adopts the held room through `GET`, no second create). That answer is the only thing the host knows: a join that returns no other home is the same answer for an unused number and for a number whose room already opened, so the sentence never claims nobody uses it and points at an invite link instead (#999) — **Enter chat** from a lone waiting room is what burns a number. A room nobody from another home ever joined leaves no Past rooms entry and no kept transcript, whether the owner left it or the joining window ran out (the host then leaves it on Bot Social). The room stays open while two or more homes are in it and closes here only when this home is the last; the closed room's dock reads "… left the room. Room closed." with one **Close**. Leave and Close go back to the last non-face chat this browser showed (else the Main Bot) and close the details panel. Countdowns use the server's `expires_at` corrected by the Bot Social clock offset (from the `Date` header, applied only at 3 s or more) and catch up on `visibilitychange`. Join stays disabled until 6 digits, an outside click closes the dialog like Escape, and Enter chat steps aside while Leave asks. Core's sidebar leads a face-room row with its number and keeps face-room turns out of a Bot row's group-work line. +6. Face-to-face join (#83, #92, #158) is a waiting room, not an instant chat: the owner picks local Bots, types the 6-digit number, then waits with the same code shown large until someone **Enter chat**s. Copy never says there is no preview. Each picker tile and each waiting-room member uses the host Bot avatar (`__flybot__.BotAvatar`), not a shared first-letter badge. The client calls `social/face-join` once per Bot with the same code and shows partial failures. If a chosen Bot has no identity, the plugin registers it first (see invariant 4); registration failures (`handle_taken`, rate limit, timeout) map to English copy and leave no leftover member. Failed join responses never leak `members` / `events` / `room_id`. Every FACE_ROOMS.md error code maps to English copy (`rate_limited` / `locked_out` include `retry_after`). The waiting room polls `social/face-rooms` (which refreshes from Bot Social, including `joinCode`) and draws one tile per person (#457: `FacePersonTile`, the person's name, `Me (you)` for the owner, with the Bots they brought underneath; the owner's tile has a tiny accent dot); **Leave** and **Enter chat** share the footer. **Enter chat** is `social/face-start` then `openSession(roomId)`, and the screen follows if someone else starts. Peer `room.message` text is untrusted: strip fence markers and wrap in `` before anything model-visible. Bot replies POST `/v1/face-rooms/{id}/messages`; `pending_approval` shows in the existing queue and in the chat (owner Approve / Reject). Owner-typed `FLYBOT-FACE-HUMAN` lines stay on that card until Approve / Always approve / Reject unless face **Auto-send** is on; the card and chat never show the raw envelope (#159). Face-room **Auto-send** is one switch per Bot of mine in the room (#457, `Auto-send Chief → room`, `social/face-auto { botId, on }`, kept in this home's `settings.faceAutoSend`, off by default): when that Bot's line comes back `pending_approval` and Bot Social says the hold is its own blanket rule (`hold_reason: 'blanket_outbound'`) or nothing but the room's Auto-send switch (`hold_reason: 'face_room_auto_send_off'` — what a clean face-room line carries once that switch is the only thing left), the host approves it at once (`decide_approval`; the switch is the owner's standing consent, so a switch that reads **on** has to send), so the room sees it; any other `hold_reason` (`risk`, `attachment`, `policy`, a value this build does not know) and other Bots' lines still wait for the owner. Only this face-room path accepts `face_room_auto_send_off` (`holdAllowsFaceAutoSend`, on top of `holdIsBlanket`); the two outbound paths keep the blanket rule alone (#865). The older account-wide `auto_approve_face_rooms` (`whoami` / MCP `set_auto_approve`) reads as every switch on; the first per-Bot change keeps the others on here and turns the account-wide one off (a `confirmation_required` answer is retried with `confirm_code` in the same click; the UI never asks for a code). REST `/v1/me` is not used with the connector token. A friend's Bot in the room member list and waiting grid uses the host Bot avatar (handle-seeded when Bot Social sends no image). New members only receive events from their join point (`after` cursor; the server also floors it). Join does not grant the Bot to friends. The UI never says published. A number nobody else is using opens no room (#458): `social/face-join` without `start` answers `{ nobody: true, faceRoomId }` when the server's room holds only this home's Bots, and the host keeps that fresh candidate off the sidebar and Past rooms (an in-memory hold, dropped on the next poll after 2 minutes). The dialog shows **Nobody is waiting under …** with **Cancel** (`social/face-leave { faceRoomId }` leaves it on Bot Social and releases a new room-only identity) and **Start a new room** (`start: true` adopts the held room through `GET`, no second create). That answer is the only thing the host knows: a join that returns no other home is the same answer for an unused number and for a number whose room already opened, so the sentence never claims nobody uses it and points at an invite link instead (#999) — **Enter chat** from a lone waiting room is what burns a number. A room nobody from another home ever joined leaves no Past rooms entry and no kept transcript, whether the owner left it or the joining window ran out (the host then leaves it on Bot Social). The room stays open while two or more homes are in it and closes here only when this home is the last; the closed room's dock reads "… left the room. Room closed." with one **Close**. Leave and Close go back to the last non-face chat this browser showed (else the Main Bot) and close the details panel. Countdowns use the server's `expires_at` corrected by the Bot Social clock offset (from the `Date` header, applied only at 3 s or more) and catch up on `visibilitychange`. Join stays disabled until 6 digits; a number that is not empty and not six digits says **Enter 6 digits** under the field (`data-flybot=face-join-digits`, zh 请输入 6 位数字, #1052) while an empty field stays quiet, an outside click closes the dialog like Escape, and Enter chat steps aside while Leave asks. Core's sidebar leads a face-room row with its number and keeps face-room turns out of a Bot row's group-work line. 7. Every import in shipped sources stays inside the plugin package: an installed plugin cannot reach into other packages' files (a repo-relative `../..` into `core/` breaks the installed bundle). 8. A friend's Bot's reply to a line you typed (or to any request) goes back through `bot.send` as an `auto-reply` and, unless Auto-send is on for that pair, is held for the replying Bot's owner (#254). The hold is decided on the reply itself (#418): the host tags the auto-reply with the chat and turn it answers (`meta.replyIn`, `turns.js` → `hooks.js` `replyRemote`), the Bot's row in `state` lists its `heldReplies` (id, status, turn, recipient; owner only, no text) and counts a waiting one in `pendingCount`, and that turn's bubble is drawn held (dashed, `core/src/client/held-reply.js`) with the decision on it (`flybot.approvals` inline entry `social-held-reply`: **Waiting for approval**, `Chief → SelfB1238`, **Approve** / **Reject** and the **Auto-send from now on** link, which approves it and turns on Auto-send for that pair only, the replying Bot → that person, through `social/auto-send { approvalId }` (#417), with the same toast as the dock card; it never touches the home-wide switch). No dock draws it, in either chat; the overlay's pointer leads to that chat. A reply that left without a card because Auto-send is on for that pair (#417) reads **Auto-sent to SelfB1238** there, quietly, so the owner sees it went without approval: the middleware's `allow` says `autoSent` (with the same `recipient`) and the host keeps that mark by turn (`state.flybot.autoSent`, last 200, no text). After the decision the same place reads **Sent to SelfB1238** or **Not sent** (muted; **Retry** on a failed send, and only when the shared `canRetry()` allows it: this plugin's own dock and the core cards read that one predicate, so a terminal failure — the Bot is gone or no longer shared, the send was refused as not permitted — draws no button anywhere, #968), and the list preview shows the friend's line instead of a reply that did not go. The card names the person who gets it (#498): the holding middleware's `recipient` (the friend's owner name, stored as `recipientName`, so the card reads the same live and after a reload), else the owner of the friend's chat; never the friend's Bot (`Chief → Chief (SelfB1238)` named two Bots); a line held for a face-to-face room names that room instead of the bare `the room` (#991), the same way the room bullet below says. A hold made before the tag (no `replyIn`) keeps its dock card as below. A friend's handle is never a room id. The sender sees nothing until it is approved. The reply also goes back to a requesting Bot that was not shared with us (the default one-way per-Bot grant): the replying Bot's grant to it is enough for a `reply`, never for a new request (#272). 9. Two holds, one click (#258): a send also held by Bot Social (`pending_approval`) is finished by the transport when the host says the owner approved *this very send* on a FlyBot card (`msg.approved`, set by `hooks.js` `release` only; a Bot-supplied field is overwritten). The approval id comes from the `send_as_member` answer, is recorded as an own line (so a failed call is retried by the poll and never shows as a card) and is decided with the same `confirm_code` step as a typed line. Room lines (`via: room` / `room-share`) keep Bot Social's hold: the friend's owner decides those. A send that left on Auto-send (#417) clears Bot Social's hold itself, the same way `autoSendFaceLine` does for a room line (#737): the owner's switch already answered that question, and leaving the hold parked the line with no card anywhere, so it never arrived. That clear only happens for the blanket hold and nothing else (#865): `holdIsBlanket()` reads Bot Social's `hold_reason` and clears the line only when it is `blanket_outbound`; every other value — `risk`, `attachment`, `policy`, a value this build does not know — keeps the line on its card, exactly as with Auto-send off. The face-room path is the one exception, and only for its own switch (#457): `autoSendFaceLine` clears `face_room_auto_send_off` too, while both outbound paths here never do. The field wins when it and `reasons` disagree. A server that sends no field keeps the old allow-list: `reasons` must be non-empty and every entry exactly `mvp.all_outbound_requires_approval`, with no `risk_flags`, so a DLP category, `dlp.blocked`, a reason this build does not know (`policy.*`), an empty `reasons` list and any risk flag all keep the line (#766) — an absent field is not read as "not blanket", or every hold on an older server would strand. A hold Bot Social has not listed yet is not "no finding" either: the line waits and the report names the approval id (`warn`), so a lagging list is diagnosable instead of silent. It reads the pending list directly rather than through `pendingApprovals()`, which would also post the waiting notice for a hold it is about to clear, and a failed `decide_approval` only warns: the hold then shows as the owner's card. Holds that were already stuck before this change need one Approve / Reject in Friends' Bots. @@ -177,7 +177,8 @@ Creating a group opens its Session with zero turns, and the host draws no sessio - 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. 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. This room is closed.` 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`). -- 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). +- 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). - Inbox and face poll share one timer: `facePollMs` (default 4s) while any live face room exists, else `inboxPollMs` (default 15s). While a FlyBot window is on screen (`flybot.watched()`, #497) a second timer also reads the inbox at the `facePollMs` cadence, so a friend's first line, the held reply's **Waiting for approval** card, the Inbox count and the row's unread badge reach the open window within a few seconds instead of up to 15 s later (the self-test's "only after a reload"). It skips a tick the shared timer already covers (a live face room) or that follows an inbox read by less than half its period, and it reads nothing with no window open, so Bot Social sees the faster reads only while someone watches. The open face overview keeps polling about every 3s after **Enter chat**, not only while matching. The social strip still refreshes past rooms about every 5s. diff --git a/patches/ds-bot/0001-core-fixes.patch b/patches/ds-bot/0001-core-fixes.patch index 8602bd4b5..95341f487 100644 --- a/patches/ds-bot/0001-core-fixes.patch +++ b/patches/ds-bot/0001-core-fixes.patch @@ -447,7 +447,7 @@ diff --git a/src/client/feedback.js b/src/client/feedback.js diff --git a/src/client/i18n.js b/src/client/i18n.js --- a/src/client/i18n.js +++ b/src/client/i18n.js -@@ -12,6 +12,48 @@ const ZH = { +@@ -12,6 +12,50 @@ const ZH = { 'Unread activity': '有未读', Search: '搜索', 'New chat': '新对话', @@ -468,6 +468,8 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js + '6-digit room number': '6 位房间号', + 'Pick a Bot first.': '请先选一个 Bot。', + 'Enter all 6 digits.': '请输入完整的 6 位数字。', ++ // #1052: visible under the field when the number is not empty and not six digits. ++ 'Enter 6 digits': '请输入 6 位数字', + 'Join with link': '用链接加入', + 'Scan a code': '扫码', + 'Join group': '加入群', diff --git a/patches/ds-bot/0008-client-bundle.patch b/patches/ds-bot/0008-client-bundle.patch index 9a6ab9c96..2e6a6f083 100644 --- a/patches/ds-bot/0008-client-bundle.patch +++ b/patches/ds-bot/0008-client-bundle.patch @@ -3,7 +3,7 @@ FlyBot patch 0008-client-bundle on Fei2411/DeepSeek-Bot@2b10c70 (ds-bot/). Gener diff --git a/client.js b/client.js --- a/client.js +++ b/client.js -@@ -49,6 +49,48 @@ +@@ -49,6 +49,50 @@ "Unread activity": "有未读", Search: "搜索", "New chat": "新对话", @@ -24,6 +24,8 @@ diff --git a/client.js b/client.js + "6-digit room number": "6 位房间号", + "Pick a Bot first.": "请先选一个 Bot。", + "Enter all 6 digits.": "请输入完整的 6 位数字。", ++ // #1052: visible under the field when the number is not empty and not six digits. ++ "Enter 6 digits": "请输入 6 位数字", + "Join with link": "用链接加入", + "Scan a code": "扫码", + "Join group": "加入群", diff --git a/plugins/social/client.js b/plugins/social/client.js index 5032f8ac0..4e40ed7c7 100644 --- a/plugins/social/client.js +++ b/plugins/social/client.js @@ -1720,7 +1720,10 @@ window.__ModuleLoader__.load({ const preview = invite?.preview ?? null const candidate = room?.status === 'candidate' const waiting = step === 'waiting' && room - const digitsTyped = String(code).replace(/\D/g, '').length === 6 + const digitCount = String(code).replace(/\D/g, '').length + const digitsTyped = digitCount === 6 + // #1052: the helper is for a started number only. Empty stays quiet; six digits clear it. + const digitsPartial = digitCount > 0 && !digitsTyped const nobody = step === 'nobody' ? faceWaitCode(room, code) : '' return h('div', { className: 'fs-dialog-back', 'data-flybot': 'face-group', role: 'dialog', 'aria-label': tr(JOIN_OR_CREATE), @@ -1792,6 +1795,9 @@ window.__ModuleLoader__.load({ disabled: busy || !selected.length || !digitsTyped, title: !selected.length ? tr('Pick a Bot first.') : digitsTyped ? undefined : tr('Enter all 6 digits.'), }, tr('Join'))), + digitsPartial ? h('div', { + className: 'fs-hint', 'data-flybot': 'face-join-digits', role: 'status', + }, tr('Enter 6 digits')) : null, h('div', { className: 'fs-link-row', 'data-flybot': 'face-link-join' }, h('input', { className: 'fs-link-input', value: invite.link, type: 'url', autoComplete: 'off', spellCheck: false, diff --git a/plugins/social/test/social.test.js b/plugins/social/test/social.test.js index b9332c05f..3778a337b 100644 --- a/plugins/social/test/social.test.js +++ b/plugins/social/test/social.test.js @@ -3607,6 +3607,46 @@ test('#306/#458: Join is enabled once a Bot is picked and all 6 digits are in; t assert.deepEqual(JSON.parse(JSON.stringify(statuses)), [{ tone: 'error', text: 'Enter a 6-digit number.' }]) }) +test('#1052: a partial room number shows Enter 6 digits and keeps Join disabled', async () => { + const { walk, pluginOf } = loadSocialClient(async () => ({})) + const bots = [{ id: 'a', name: 'Chief', isMain: true }] + const view = (code, selected = ['a']) => walk(pluginOf(['setup', bots, selected, code]).FaceToFaceGroup({})) + const helper = (tree) => tree.find(n => n.props['data-flybot'] === 'face-join-digits') + const join = (tree) => tree.find(n => n.type === 'button' && n.props.type === 'submit') + const text = (node) => (node?.children ?? []).map(child => (typeof child === 'string' ? child : '')).join('') + + const five = view('58291') + assert.equal(text(helper(five)), 'Enter 6 digits') + assert.equal(join(five).props.disabled, true) + assert.equal(five.find(n => n.type === 'input' && n.props.className === 'fs-digits').props.placeholder, '6 digits') + assert.ok(five.some(n => n.props['data-flybot'] === 'face-link-join'), 'the invite-link field stays on the same screen') + + const one = view('5') + assert.equal(text(helper(one)), 'Enter 6 digits') + assert.equal(join(one).props.disabled, true) + + // Digits are what count: letters in a forced state are not a six-digit code. + const mixed = view('12ab') + assert.equal(text(helper(mixed)), 'Enter 6 digits') + assert.equal(join(mixed).props.disabled, true) + + const empty = view('') + assert.equal(helper(empty), undefined, 'an empty field stays quiet') + assert.equal(join(empty).props.disabled, true) + + const six = view('582913') + assert.equal(helper(six), undefined, 'six digits clear the helper') + assert.equal(join(six).props.disabled, false) + + const unpicked = view('58291', []) + assert.equal(text(helper(unpicked)), 'Enter 6 digits') + assert.equal(join(unpicked).props.disabled, true) + assert.ok(unpicked.some(n => n.props['data-flybot'] === 'face-join-pick')) + + const { DICTIONARY } = await import('../../../core/src/client/i18n.js') + assert.equal(DICTIONARY.zh['Enter 6 digits'], '请输入 6 位数字') +}) + test('#306: the waiting dialog asks before Leave, lands on the main Bot, and the panel draws no second waiting card', async () => { const calls = [] const { walk, pluginOf } = loadSocialClient(async (endpoint, payload) => { calls.push([endpoint, payload]); return {} })