From 851cfdc7e97126eb42e64db96705daab56baace3 Mon Sep 17 00:00:00 2001 From: Fredrik Johansson Date: Mon, 29 Jun 2026 19:27:44 +0200 Subject: [PATCH] fix: reactions in browser mode (BrowserAdapter) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- web/src/adapter/browser.ts | 48 +++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/web/src/adapter/browser.ts b/web/src/adapter/browser.ts index 8aa8c6d..78b94f5 100644 --- a/web/src/adapter/browser.ts +++ b/web/src/adapter/browser.ts @@ -433,9 +433,17 @@ class PeerConn { nick: m['nick'] as string || '', caps: m['caps'] || [] }) } else if (m['type'] === 'chat') { + const ts = (m['ts'] as number) || Date.now() this.on('chat', { 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') { 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 */ } } - sendChat(room: string, text: string) { - this._dc({ type: 'chat', room, text, ts: Date.now() }) + sendChat(room: string, text: string, mid: string) { + this._dc({ type: 'chat', room, text, ts: Date.now(), mid }) } sendPm(text: string) { 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) { 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() }, }) + } 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') { const ts = (data['ts'] as number) || Date.now() - const mid = `${data['peer']}-${ts}` + const mid = (data['mid'] as string) || `${data['peer']}-${ts}` this.emit({ type: 'message_received', network_id: this.networkId, @@ -910,8 +930,8 @@ export class BrowserAdapter { }, }) } else { - // Broadcast - this.peers.forEach(p => p.sendChat(room, text)) + // Broadcast — include mid on the wire so recipients can reference it in reactions + this.peers.forEach(p => p.sendChat(room, text, mid)) this.emit({ type: 'message_received', network_id: this.networkId, @@ -924,6 +944,22 @@ export class BrowserAdapter { 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.passphrase) return try {