Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/343-invite-ux.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add friend is easier to follow: pasting and **Accept invite** come first on a full-width box, inviting is its own line named for the Bot it shares (**Invite a friend to Chief**) and shows the link it made, and after **Accept invite** the panel stays open with what happened (`Alice shared Helper with you.`, or a plain note when no Bot arrived) until you press Done ([#343](https://github.com/ThinkFlowLab/FlyBot/issues/343), part of it).
4 changes: 2 additions & 2 deletions docs/subsystems/social.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ pins the list. The authority for the list is Bot Social's README and `docs/FACE_
- #191: a peer's leave reads `Bob left the room` (the person, from the member's owner name; the Bot label only when there is no owner name), and an ended face room shows a plain status line above the composer (`FaceRoomStatus` on `flybot.session.dock`, `data-flybot=face-room-end`: `Bob left the room. This room is closed.` / `You left this room.`). Joining by number opens the new room in the main pane at once, so the top bar never keeps the previous room's title.
- #197: while that status line shows, the composer is hidden (a CSS rule on the composer stack that has `data-flybot=face-room-end`), so a left or closed room is read-only. The account-menu handle uses normal ink. User-facing text no longer names a Friends' Bots section: `relationship_exists` reads `You're already friends. Their Bot is back in your list.`, and a held message to a friend's Bot says it waits for approval in this chat and names the Auto-send setting.
- My own handle (`@name.home`) shows with a **Copy** link at the top of the account menu (slot `flybot.account.header`, `MyHandle`) as well as in Add friend (#181).
- Sidebar **+** is a short action menu, not the To:/composer. Add friend has one-click **Copy invite link** (mint + clipboard) with a Bot picker when there is more than one Bot, and a `You are @your.home` line (#181). **Talk through** stays on one line (nowrap label). Bot Sharing puts the invite link on its own full-width row above one action row: **Copy link** (the one primary button), **Copy with instructions**, and a quiet **New link** text button (#230); before the first link there is only **Invite to this Bot**. The **Sharing** heading uses the same 4px inset as the core settings headings. The social strip keeps the **N to approve** badge and opens that chat; the stacked jump list stays out of the column. Held `face_room_message` drafts show in that face chat with readable names (`Chief → the room`), never raw `bot_01…` or a `FLYBOT-FACE-HUMAN` envelope.
- Sidebar **+** is a short action menu, not the To:/composer. Add friend opens with the paste box (full width, `Paste invite link or code`) and **Accept invite**, then a separate **Invite a friend to {Bot}** button (mint + clipboard; the Bot is always named, with a picker when there is more than one) that leaves the link in a read-only field below it, and a `You are @your.home` line (#181). After **Accept invite** the panel stays open with the result until **Done**: `{person} shared {Bot} with you.`, or, when the join brought no Bot, `Joined {person}'s invite, but their Bot hasn't loaded yet…` (#343). **Talk through** stays on one line (nowrap label). Bot Sharing puts the invite link on its own full-width row above one action row: **Copy link** (the one primary button), **Copy with instructions**, and a quiet **New link** text button (#230); before the first link there is only **Invite to this Bot**. The **Sharing** heading uses the same 4px inset as the core settings headings. The social strip keeps the **N to approve** badge and opens that chat; the stacked jump list stays out of the column. Held `face_room_message` drafts show in that face chat with readable names (`Chief → the room`), never raw `bot_01…` or a `FLYBOT-FACE-HUMAN` envelope.

### Face-room human turns and UI polish (#166, #167)

Expand Down Expand Up @@ -145,7 +145,7 @@ pins the list. The authority for the list is Bot Social's README and `docs/FACE_
- 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). 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.
- Sidebar **+** holds Face-to-face group, Add friend, Accept invite, one-click Copy invite link, and Copy my handle. Friend requests is not a duplicate + row. Per-friend Auto-send is on that Bot's settings; face-room Auto-send is on the open room overview — both say **Auto-send** ("Don't ask each time."). Hold labels resolve the real Bot name (`Chief → the room`), never `bot_01…`.
- Sidebar **+** holds Face-to-face group, Add friend, Accept invite, one-click Copy invite link (right-click a Bot), and Copy my handle. Friend requests is not a duplicate + row. Per-friend Auto-send is on that Bot's settings; face-room Auto-send is on the open room overview — both say **Auto-send** ("Don't ask each time."). Hold labels resolve the real Bot name (`Chief → the room`), never `bot_01…`.

### Face-to-face two-instance rooms (#117)

Expand Down
44 changes: 28 additions & 16 deletions plugins/social/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -1353,7 +1353,7 @@ window.__ModuleLoader__.load({
}

// Add friend: join a friend's link and answer requests (invites are made from a Bot's right-click menu).
function AddFriend({ onChange, onDone, actions } = {}) {
function AddFriend({ onChange, onDone } = {}) {
const [overview, setOverview] = useState(null)
const [code, setCode] = useState('')
const [status, setStatus] = useState(null)
Expand All @@ -1364,7 +1364,11 @@ window.__ModuleLoader__.load({
// Which of my Bots the copied invite shares (default: the Main Bot), and my own handle (#181).
const [sharing, setSharing] = useState('')
const [me, setMe] = useState('')
const refresh = () => call('social/overview', {}).then(setOverview, e => setStatus({ tone: 'error', text: errorText(e) }))
// The link that was just made, kept on screen so it can be checked or copied by hand (#343).
const [made, setMade] = useState(null)
// `keep`: a result is on screen (an accepted invite, say); a failed re-read of the friend list must not
// replace it with an error that looks like the action failed (#343).
const refresh = (keep = false) => call('social/overview', {}).then(setOverview, (e) => { if (!keep) setStatus({ tone: 'error', text: errorText(e) }) })
useEffect(() => {
refresh()
call('social/status', {}).then(value => {
Expand All @@ -1389,7 +1393,7 @@ window.__ModuleLoader__.load({
try {
const message = await fn()
if (message) setStatus({ tone: 'ok', text: message })
await refresh()
await refresh(Boolean(message))
onChange?.()
} catch (e) {
const text = errorText(e)
Expand All @@ -1401,19 +1405,21 @@ window.__ModuleLoader__.load({
const mint = () => run('Invite', async () => {
const botId = sharing || mine.find(bot => bot.isMain)?.id || mine[0]?.id
if (!botId) throw new Error('Choose one of your Bots to invite a friend to')
const botName = mine.find(bot => bot.id === botId)?.name || 'your Bot'
const invite = await call('social/create-invite', { botId })
const link = invite?.link || invite?.url
if (!link) throw new Error('The invite came back without its link')
setMade({ link, botName })
const clip = typeof navigator !== 'undefined' ? navigator.clipboard : undefined
if (clip?.writeText) {
try {
await clip.writeText(link)
return 'Invite link copied. Send it to a friend.'
return `Invite link for ${botName} copied. Send it to a friend.`
} catch {
return `Could not copy. Link: ${link}`
return `Could not copy. Copy the link below and send it to a friend.`
}
}
return `Invite link: ${link}`
return 'Copy the link below and send it to a friend.'
})
const join = e => {
e.preventDefault()
Expand All @@ -1423,11 +1429,9 @@ window.__ModuleLoader__.load({
const who = r.inviterName ?? r.inviter ?? 'Your friend'
const shared = r.inviterBotName || r.sharedBot
if (r.status === 'active') {
// The new Bot row is in the sidebar already: close this panel and say what was added,
// instead of leaving it open until Done (#195).
const text = shared ? `${who} shared ${shared} with you.` : `${who} shared a Bot with you.`
if (onDone && actions?.notify) { actions.notify(text); setTimeout(onDone, 0) }
return text
// The panel stays open with the result until Done, so it can be read (#343). A join that
// brought no Bot says so instead of claiming one was shared.
return shared ? `${who} shared ${shared} with you.` : `Joined ${who}'s invite, but their Bot hasn't loaded yet. It shows in your list when it does; if not, accept the link again.`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the accepted-invite result when refresh fails

When joining succeeds but the subsequent social/overview refresh fails (for example, a transient list_friends error), refresh() replaces this success text with its own error via setStatus. Previously the success was preserved in a notification and the panel closed; now the panel remains open and the only result the user sees incorrectly looks like an acceptance failure. Keep the join result separate from refresh errors, or prevent the post-success refresh from overwriting it.

Useful? React with 👍 / 👎.

}
return `Request sent to ${who}. They show up here once they accept.`
})
Expand All @@ -1443,20 +1447,28 @@ window.__ModuleLoader__.load({
h('div', { className: 'fs-label fs-label-plain' }, 'Add friend'),
onDone ? h('button', { type: 'button', className: 'fs-btn fs-btn-sm', onClick: onDone }, 'Done') : null),
me ? h('div', { className: 'fs-hint', 'data-flybot': 'my-handle', style: { padding: '0 10px' } }, `You are ${me}`) : null,
h('div', { className: 'fs-line', style: { padding: '4px 10px 0' }, 'data-flybot': 'invite-line' },
mine.length > 1 ? h('select', { className: 'fs-input', value: sharing, 'aria-label': 'Which of your Bots to share', 'data-flybot': 'share-bot', onChange: e => setSharing(e.target.value) },
mine.map(bot => h('option', { key: bot.id, value: bot.id }, bot.name))) : null,
h('button', { type: 'button', className: 'fs-btn', 'data-flybot': 'mint-invite', disabled: busy, onClick: mint }, 'Copy invite link')),
h('form', { onSubmit: join, className: 'fs-card bt-social-join' },
// The paste box takes the full width on its own row, so the start of a link stays readable (#343).
h('input', { className: 'fs-input', style: { flex: '0 0 auto', width: '100%', boxSizing: 'border-box' }, value: code, placeholder: 'Paste invite link or code', 'aria-label': 'Invite link or code', onChange: e => setCode(e.target.value) }),
h('div', { className: 'fs-line' },
h('input', { className: 'fs-input', value: code, placeholder: 'Paste invite link', 'aria-label': 'Invite link or code', onChange: e => setCode(e.target.value) }),
h('button', { type: 'submit', className: 'fs-btn', disabled: busy || !code.trim() }, 'Accept invite')),
// Accepting grants only the invited Bot to you. The Bot you talk through is not shared, so
// there is nothing to pick with one Bot (#228).
mine.length > 1 ? h('label', { className: 'fs-line fs-hint fs-join-as', 'data-flybot': 'join-as', title: "Talks for you. Not shared: your friend can't use it unless you invite them to it." },
h('span', { className: 'fs-join-as-label', 'data-flybot': 'talk-through' }, 'Talk through '),
h('select', { className: 'fs-input', value: joiner, 'aria-label': 'Which of your Bots talks', onChange: e => setJoiner(e.target.value) },
mine.map(bot => h('option', { key: bot.id, value: bot.id }, bot.name)))) : null),
// Inviting is its own named line: which Bot the link shares is always said, and the link is shown once made.
h('div', { className: 'fs-line', style: { padding: '4px 10px 0' }, 'data-flybot': 'invite-line' },
mine.length > 1 ? h('select', { className: 'fs-input', value: sharing, 'aria-label': 'Which of your Bots to share', 'data-flybot': 'share-bot', onChange: e => { setSharing(e.target.value); setMade(null) } },
mine.map(bot => h('option', { key: bot.id, value: bot.id }, bot.name))) : null,
(() => {
const label = `Invite a friend to ${(mine.find(bot => bot.id === sharing) ?? mine.find(bot => bot.isMain) ?? mine[0])?.name || 'your Bot'}`
// A long Bot name shrinks the button with an ellipsis instead of being clipped by the sidebar, so the picker stays usable.
return h('button', { type: 'button', className: 'fs-btn', 'data-flybot': 'mint-invite', title: label, style: { flex: '0 1 auto', minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' }, disabled: busy, onClick: mint }, label)
})()),
made ? h('div', { className: 'fs-line', style: { padding: '2px 10px 0' } },
h('input', { className: 'fs-input', readOnly: true, value: made.link, 'aria-label': `Invite link for ${made.botName}`, 'data-flybot': 'invite-link', onFocus: e => e.target.select?.(), onClick: e => e.target.select?.() })) : null,
status ? h('div', { className: 'fs-status', role: 'status', 'data-tone': status.tone, style: { padding: '2px 10px' } }, status.text) : null,
incoming.length ? h('div', { className: 'bt-social-requests' },
h('div', { className: 'fs-label' }, 'Friend requests', h('span', { className: 'fs-count' }, String(incoming.length))),
Expand Down
Loading
Loading