fix: reactions in browser mode (BrowserAdapter)

- Include mid on the wire in sendChat() so recipients can reference it
- Handle incoming 'reaction' wire message in PeerConn._onControl
- Add PeerConn.sendReaction() to send the reaction wire message
- Handle 'send_reaction' IPC command in BrowserAdapter.send()
- Emit 'reaction' IPC event on receive and on local send
- Use wire mid (not synthetic peer-ts mid) when receiving chat messages

Reactions now work across all combinations: daemon↔daemon,
browser↔browser, and daemon↔browser.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-06-29 19:27:44 +02:00
parent 4a7a95fe9d
commit 851cfdc7e9

View File

@@ -433,9 +433,17 @@ class PeerConn {
nick: m['nick'] as string || '', caps: m['caps'] || [] nick: m['nick'] as string || '', caps: m['caps'] || []
}) })
} else if (m['type'] === 'chat') { } else if (m['type'] === 'chat') {
const ts = (m['ts'] as number) || Date.now()
this.on('chat', { this.on('chat', {
peer: this.peerId, room: m['room'] as string || 'general', peer: this.peerId, room: m['room'] as string || 'general',
text: m['text'] as string, ts: m['ts'] as number || Date.now() text: m['text'] as string, ts,
mid: (m['mid'] as string) || `${this.peerId}-${ts}`,
})
} else if (m['type'] === 'reaction') {
this.on('reaction', {
peer: this.peerId,
mid: m['reaction_mid'] as string,
emoji: m['reaction_emoji'] as string,
}) })
} else if (m['type'] === 'pm') { } else if (m['type'] === 'pm') {
this.on('pm', { peer: this.peerId, text: m['text'] as string, ts: m['ts'] as number || Date.now() }) this.on('pm', { peer: this.peerId, text: m['text'] as string, ts: m['ts'] as number || Date.now() })
@@ -556,14 +564,18 @@ class PeerConn {
} catch { /* ignore */ } } catch { /* ignore */ }
} }
sendChat(room: string, text: string) { sendChat(room: string, text: string, mid: string) {
this._dc({ type: 'chat', room, text, ts: Date.now() }) this._dc({ type: 'chat', room, text, ts: Date.now(), mid })
} }
sendPm(text: string) { sendPm(text: string) {
this._dc({ type: 'pm', text, ts: Date.now() }) this._dc({ type: 'pm', text, ts: Date.now() })
} }
sendReaction(mid: string, emoji: string) {
this._dc({ type: 'reaction', reaction_mid: mid, reaction_emoji: emoji })
}
private _dc(obj: object) { private _dc(obj: object) {
if (this.dc?.readyState === 'open') this.dc.send(JSON.stringify(obj)) if (this.dc?.readyState === 'open') this.dc.send(JSON.stringify(obj))
} }
@@ -755,9 +767,17 @@ export class BrowserAdapter {
public_key: data['peer'] as string, created_at: new Date().toISOString() public_key: data['peer'] as string, created_at: new Date().toISOString()
}, },
}) })
} else if (event === 'reaction') {
this.emit({
type: 'reaction',
network_id: this.networkId,
peer_id: data['peer'] as unknown as import('../types').PeerID,
reaction_mid: data['mid'] as string,
reaction_emoji: data['emoji'] as string,
})
} else if (event === 'chat') { } else if (event === 'chat') {
const ts = (data['ts'] as number) || Date.now() const ts = (data['ts'] as number) || Date.now()
const mid = `${data['peer']}-${ts}` const mid = (data['mid'] as string) || `${data['peer']}-${ts}`
this.emit({ this.emit({
type: 'message_received', type: 'message_received',
network_id: this.networkId, network_id: this.networkId,
@@ -910,8 +930,8 @@ export class BrowserAdapter {
}, },
}) })
} else { } else {
// Broadcast // Broadcast — include mid on the wire so recipients can reference it in reactions
this.peers.forEach(p => p.sendChat(room, text)) this.peers.forEach(p => p.sendChat(room, text, mid))
this.emit({ this.emit({
type: 'message_received', type: 'message_received',
network_id: this.networkId, network_id: this.networkId,
@@ -924,6 +944,22 @@ export class BrowserAdapter {
return return
} }
if (msg.type === 'send_reaction') {
const mid = msg.reaction_mid
const emoji = msg.reaction_emoji
if (!mid || !emoji) return
this.peers.forEach(p => p.sendReaction(mid, emoji))
// Emit locally so the sender sees their own reaction immediately
this.emit({
type: 'reaction',
network_id: this.networkId,
peer_id: this.identity.id as unknown as import('../types').PeerID,
reaction_mid: mid,
reaction_emoji: emoji,
})
return
}
if (msg.type === 'export_identity') { if (msg.type === 'export_identity') {
if (!msg.passphrase) return if (!msg.passphrase) return
try { try {