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/1640-waiting-card-destination.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The "Waiting for the owner's approval" card in a group chat now says where the room's approval is decided: in a Bot's chat, with one button that opens it, or on the reply bubble above it, which is already on screen. When the dock cannot place the card it says only what it said before ([#1640](https://github.com/ThinkFlowLab/FlyBot/issues/1640)).
2 changes: 2 additions & 0 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ var ZH = {
"no longer shared": "已不再共享",
"Messages waiting for your approval": "等你批准的消息",
"Waiting for approval": "等待批准",
"Decide on the reply bubble above.": "在上面的回复气泡里批准。",
"Decide in {name}'s chat.": "在 {name} 的聊天里批准。",
"Not sent": "未发出",
"{name} · Not sent": "{name} · 未发出",
"Sent to {to}": "已发给 {to}",
Expand Down
2 changes: 2 additions & 0 deletions core/src/client/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ const ZH = {
'no longer shared': '已不再共享',
'Messages waiting for your approval': '等你批准的消息',
'Waiting for approval': '等待批准',
'Decide on the reply bubble above.': '在上面的回复气泡里批准。',
"Decide in {name}'s chat.": '在 {name} 的聊天里批准。',
'Not sent': '未发出',
'{name} · Not sent': '{name} · 未发出',
'Sent to {to}': '已发给 {to}',
Expand Down
4 changes: 3 additions & 1 deletion patches/ds-bot/0001-core-fixes.patch
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
Add: '添加',
Close: '关闭',
Copy: '复制',
@@ -41,22 +41,71 @@ const ZH = {
@@ -41,22 +41,73 @@ const ZH = {
' and ': '、',
and: '和',
'{count} members': '{count} 位成员',
Expand Down Expand Up @@ -775,6 +775,8 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
+ 'no longer shared': '已不再共享',
+ 'Messages waiting for your approval': '等你批准的消息',
+ 'Waiting for approval': '等待批准',
+ 'Decide on the reply bubble above.': '在上面的回复气泡里批准。',
+ "Decide in {name}'s chat.": '在 {name} 的聊天里批准。',
+ 'Not sent': '未发出',
+ '{name} · Not sent': '{name} · 未发出',
+ 'Sent to {to}': '已发给 {to}',
Expand Down
4 changes: 3 additions & 1 deletion patches/ds-bot/0008-client-bundle.patch
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ diff --git a/client.js b/client.js
Add: "添加",
Close: "关闭",
Copy: "复制",
@@ -78,21 +78,70 @@ var ZH = {
@@ -78,21 +78,72 @@ var ZH = {
" and ": "、",
and: "和",
"{count} members": "{count} 位成员",
Expand Down Expand Up @@ -196,6 +196,8 @@ diff --git a/client.js b/client.js
+ "no longer shared": "已不再共享",
+ "Messages waiting for your approval": "等你批准的消息",
+ "Waiting for approval": "等待批准",
+ "Decide on the reply bubble above.": "在上面的回复气泡里批准。",
+ "Decide in {name}'s chat.": "在 {name} 的聊天里批准。",
+ "Not sent": "未发出",
+ "{name} · Not sent": "{name} · 未发出",
+ "Sent to {to}": "已发给 {to}",
Expand Down
43 changes: 40 additions & 3 deletions plugins/social/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2071,13 +2071,25 @@ window.__ModuleLoader__.load({
const words = (Array.isArray(reasons) ? reasons : []).filter(name => Object.hasOwn(HOLD_REASON_WORDS, name)).map(name => tr(HOLD_REASON_WORDS[name]))
return words.length ? tr('Held for your approval: contains {reasons}', { reasons: words.join(tr(', ')) }) : ''
}
function ChatApproval({ item, canDecide = true, busy, onDecide, t } = {}) {
function ChatApproval({ item, canDecide = true, busy, onDecide, t, onOpen } = {}) {
const [open, setOpen] = useState(false)
// A decided card goes at once, no "Approved" / "Rejected" stub (#456): the chat itself shows the outcome.
if (!item || item.status === 'approved' || item.status === 'rejected') return null
if (canDecide !== true) {
// #1640: name where this card is decided, and open it when that is a chat. Nothing to name keeps
// the neutral sentence, and a destination already on screen gets no button.
const tr = (text, params) => (typeof t === 'function'
? t(text, params)
: String(text).replace(/\{(\w+)\}/g, (_, key) => String(params?.[key] ?? '')))
const place = item.place
return h('div', { className: 'fs-hold fs-chat-approval', 'data-flybot': 'chat-approval', 'data-role': 'other', role: 'status' },
h('div', { className: 'fs-name' }, WAITING_OTHERS))
h('div', { className: 'fs-name' }, WAITING_OTHERS),
place?.bubble ? h('div', { className: 'fs-sub', 'data-flybot': 'waiting-place' }, tr('Decide on the reply bubble above.')) : null,
place?.chatId ? h('div', { className: 'fs-sub', 'data-flybot': 'waiting-place' }, tr("Decide in {name}'s chat.", { name: place.name })) : null,
place?.chatId && typeof onOpen === 'function'
? h('div', { className: 'fs-actions fs-chat-actions' },
h('button', { type: 'button', className: 'fs-btn fs-btn-sm', 'data-flybot': 'chat-approval-open-chat', onClick: () => onOpen(place.chatId) }, tr("Open {name}'s chat", { name: place.name })))
: null)
}
if (item.status === 'failed') {
// A failed send cannot be approved or rejected: say it did not go and offer Retry, like
Expand Down Expand Up @@ -2477,9 +2489,34 @@ window.__ModuleLoader__.load({
const approveAll = async () => {
for (const item of mine.filter(waiting)) await decide(item, 'approve')
}
// #1640: this card is drawn exactly when the room's own card is decided somewhere this chat does
// not draw. A held reply is decided on its own bubble here (#418); a Bot's hold is decided in a
// chat this person can open (#254). A card we cannot place keeps the neutral sentence: the dock
// never invents a destination.
const waitingPlace = () => {
// A room-tagged card belongs to the room that held it (#188): the same scoping the room's own
// list is read with (#1639), applied here to the two polls that know the destination.
const roomIds = new Set([room?.id, room?.face?.roomId].filter(Boolean))
const shown = new Set(mine.map(card => card.id))
const away = [...held, ...local.filter(item => item.inline)]
.filter(item => !shown.has(item.id) && (!item.roomId || roomIds.has(item.roomId)))
if (away.some(item => item.inline === sessionId)) return { bubble: true }
for (const item of away) {
// A held reply is decided where it is drawn: its own bubble, never a chat to open.
if (item.inline) continue
const friend = listedFriendChat(roster, typeof item.to === 'object' && item.to ? item.to.handle : item.to)
const own = item.fromId ? (roster?.byId?.[item.fromId] ?? (roster?.bots ?? []).find(bot => bot.id === item.fromId)) : undefined
const chat = friend ?? own
if (chat?.id) return { chatId: chat.id, name: chat.name ?? chat.title ?? '' }
}
return undefined
}
if (waitingOthers) {
return h('div', { 'data-flybot': 'chat-approvals' },
h(ChatApproval, { item: { id: 'waiting' }, canDecide: false }))
h(ChatApproval, {
item: { id: 'waiting', place: waitingPlace() }, canDecide: false, t,
onOpen: chatId => actions?.openSession?.(chatId),
}))
}
return h('div', { 'data-flybot': 'chat-approvals' },
mine.length > 1 && mine.some(waiting)
Expand Down
74 changes: 74 additions & 0 deletions plugins/social/test/approval-dock.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,77 @@ test('#1638: a room with no pending card and a zero count draws no card', () =>
assert.equal(tree, '', 'a quiet room draws no card')
} finally { delete globalThis.__flybot__ }
})

// #1640: the waiting card said nothing about where the room's card is. A held reply is decided on
// its own bubble in this chat (#418); a local Bot's hold is decided in that Bot's chat (#254).
const coder = { id: 'bot1', name: 'Coder', color: 'blue' }
const botHold = () => ({ id: 'a2', status: 'pending', senderName: 'SelfTest A2', request: { text: 'hi', from: 'bot1', to: { handle: '@friend.home' }, via: 'auto' } })
const inlineHold = () => ({ id: 'a3', status: 'pending', senderName: 'SelfTest A2', request: { text: 'hi', from: 'bot1', to: { handle: '@friend.home' }, via: 'auto-reply', meta: { replyIn: { sessionId: 's1' } } } })
// The button an approval card offers, found the way the rest of this file walks the tree.
const button = (node, id) => {
if (Array.isArray(node)) return node.map(child => button(child, id)).find(Boolean)
if (!node || typeof node !== 'object') return undefined
if (node.props?.['data-flybot'] === id) return node
if (typeof node.type === 'function') return button(mount(node), id)
return button(node.children, id)
}
async function settle(hold) {
const app = loadDock()
const { state, inst } = harness()
const opened = []
inst.props.roster = { bots: [coder], byId: { bot1: coder }, roomsById: {} }
inst.props.actions = { openSession: id => opened.push(id) }
render(app.ChatApprovalDock, inst)
runEffects(inst)
state.resolveApprovals(hold)
await tick()
return { app, inst, opened, tree: render(app.ChatApprovalDock, inst) }
}

test("#1640: a hold written by a local Bot names that Bot's chat and opens it", async () => {
const { opened, tree } = await settle([botHold()])
try {
const text = drawn(tree)
assert.match(text, /Waiting for the owner's approval/, 'the waiting card still says it is a card we cannot decide here')
assert.match(text, /Decide in Coder's chat\./, 'the card names the Bot chat the card is decided in')
const open = button(tree, 'chat-approval-open-chat')
assert.ok(open, 'the card offers the way there')
assert.equal(open.props.type, 'button')
assert.equal(drawn(open), "Open Coder's chat")
open.props.onClick()
assert.deepEqual(opened, ['bot1'], 'one click opens that Bot chat')
assert.doesNotMatch(text, /Approve/, 'the waiting card never offers a decision here')
} finally { delete globalThis.__flybot__ }
})

test('#1640: a held reply names the reply bubble above and offers no jump, because it is on screen', async () => {
const { tree } = await settle([inlineHold()])
try {
const text = drawn(tree)
assert.match(text, /Waiting for the owner's approval/)
assert.match(text, /Decide on the reply bubble above\./, 'the card names the reply bubble it is decided on')
assert.equal(button(tree, 'chat-approval-open-chat'), undefined, 'nothing to jump to: the bubble is here')
assert.doesNotMatch(text, /Opens? .* chat/, 'no destination is invented')
} finally { delete globalThis.__flybot__ }
})

test('#1640: a held reply from another chat is not this room\'s bubble', async () => {
// `local` is every local hold, not only this chat's: a bubble in another chat is not "above" here.
const { tree } = await settle([{ ...inlineHold(), id: 'a4', request: { ...inlineHold().request, meta: { replyIn: { sessionId: 's9' } } } }])
try {
const text = drawn(tree)
assert.match(text, /Waiting for the owner's approval/)
assert.doesNotMatch(text, /Decide on the reply bubble above/, 'a bubble in another chat is not this chat\'s bubble')
assert.equal(button(tree, 'chat-approval-open-chat'), undefined, 'and it is no chat to jump to either')
} finally { delete globalThis.__flybot__ }
})

test('#1640: a room count this dock cannot place keeps the neutral sentence and no button', async () => {
const { tree } = await settle([])
try {
const text = drawn(tree)
assert.match(text, /Waiting for the owner's approval/, 'the neutral sentence stays')
assert.equal(button(tree, 'chat-approval-open-chat'), undefined, 'an unknown destination draws no jump')
assert.doesNotMatch(text, /Decide in |Decide on the reply bubble/, 'no destination is invented')
} finally { delete globalThis.__flybot__ }
})
Loading