borogove
created pr with
borogove/multi-dm.1
added borogove/multi-dm.2
1: 0ef2769 = 1: 41e89c8 Use to for chatId if not in recipients
2: 90e5a6a = 2: c323e3d clone should clone attributes
3: d86ea28 = 3: a1199ae Actually send to all recipients
4: 1c1061f ! 4: 94bf87d Never add self as counterpart
5: 9c8aa41 = 5: 32b9fad Extended addressing for sent messages specifies all recipients
6: 2da9124 = 6: ce4d9e1 Always add all recipients to outbound message
7: e7db006 = 7: 350343e Use a shared helper for adding extended addressing
8: 4530ece = 8: 40f662a Add startMultiChat to create multiDMs
9: 6810ba9 = 9: 8043c53 Never include ourselves in incoming chat ids
-: ------- > 10: 68e7246 Cannot MAM or bookmark a multiDM
cmds
checkout latest patchset:
ssh pr.pico.sh pull borogove:multi-dm | git am -3checkout specific patchset revision:
ssh pr.pico.sh pull borogove:multi-dm [rev] | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh borogove:multi-dmadd comment to patch request:
ssh pr.pico.sh comment borogove:multi-dm "lgtm!"
Patchset
borogove/multi-dm.2
Use to for chatId if not in recipients
Stephen Paul Weber
clone should clone attributes
2026-11-08T19:15:14ZStephen Paul Weber
Actually send to all recipients
2026-11-09T16:18:06ZStephen Paul Weber
Never add self as counterpart
2026-11-09T16:24:50ZStephen Paul Weber
Extended addressing for sent messages specifies all recipients
2026-11-08T19:20:04ZStephen Paul Weber
Always add all recipients to outbound message
2026-11-08T19:20:46ZStephen Paul Weber
→ Use a shared helper for adding extended addressing
2026-11-08T19:21:50ZStephen Paul Weber
Add startMultiChat to create multiDMs
2026-11-08T19:32:30ZStephen Paul Weber
Never include ourselves in incoming chat ids
2026-11-08T21:01:35ZStephen Paul Weber
Cannot MAM or bookmark a multiDM
2026-11-09T19:40:59ZStephen Paul Weber
2026-11-10T17:34:01Z
Use a shared helper for adding extended addressing
Stephen Paul Weber
2026-11-08T19:32:30ZSemantic diff summary
0 added,
0 modified,
0 signature changed,
0 removed
across 0 analyzed files
(4 files skipped: unsupported file type)
+1
-8
borogove/Chat.hx
#
| ... | ... | @@ -1370,7 +1370,7 @@ class DirectChat extends Chat { | |
| 1370 | 1370 | final update = new ReactionUpdate(ID.unique(), null, null, m.localId, m.chatId(), client.accountId(), Date.format(std.Date.now()), reactions, EmojiReactions); | |
| 1371 | 1371 | final outboxItem = outbox.newItem(); | |
| 1372 | 1372 | persistence.storeReaction(client.accountId(), update).then((stored) -> { | |
| 1373 | - | sendMessageStanza(update.asStanza(), outboxItem); | |
| 1373 | + | sendMessageStanza(Util.addExtendedAddressing(update.asStanza(), _fullCounterparts.map((p) -> JID.parse(p))), outboxItem); | |
| 1374 | 1374 | if (stored != null) client.notifyMessageHandlers(stored, ReactionEvent); | |
| 1375 | 1375 | }); | |
| 1376 | 1376 | } |
| ... | ... | @@ -1384,13 +1384,6 @@ class DirectChat extends Chat { | |
| 1384 | 1384 | thenshim.PromiseTools.all(counters.map(counterpart -> { | |
| 1385 | 1385 | final clone = stanza.clone(); | |
| 1386 | 1386 | clone.attr.set("to", counterpart); | |
| 1387 | - | if (counters.length > 1 && stanza.getChild("addresses", "http://jabber.org/protocol/address") == null) { | |
| 1388 | - | final addresses = clone.tag("addresses", { xmlns: "http://jabber.org/protocol/address" }); | |
| 1389 | - | for (counter in counters) { | |
| 1390 | - | addresses.tag("address", { type: "to", jid: counter, delivered: "true" }).up(); | |
| 1391 | - | } | |
| 1392 | - | addresses.up(); | |
| 1393 | - | } | |
| 1394 | 1387 | ||
| 1395 | 1388 | // Default should try all preferences in order? | |
| 1396 | 1389 | // Otherwise force to the one we were passed? |
+2
-7
borogove/ChatMessage.hx
#
| ... | ... | @@ -640,13 +640,8 @@ class ChatMessage { | |
| 640 | 640 | var stanza = new Stanza("message", attrs); | |
| 641 | 641 | if (versions.length > 0 && versions[versions.length-1].localId != null) stanza.tag("replace", { xmlns: "urn:xmpp:message-correct:0", id: versions[versions.length-1].localId }).up(); | |
| 642 | 642 | if (threadId != null) stanza.textTag("thread", threadId); | |
| 643 | - | if (recipients.length > 1) { | |
| 644 | - | final addresses = stanza.tag("addresses", { xmlns: "http://jabber.org/protocol/address" }); | |
| 645 | - | for (recipient in recipients) { | |
| 646 | - | addresses.tag("address", { type: "to", jid: recipient.asString(), delivered: "true" }).up(); | |
| 647 | - | } | |
| 648 | - | addresses.up(); | |
| 649 | - | } else if (recipients.length == 1 && to == null) { | |
| 643 | + | Util.addExtendedAddressing(stanza, recipients); | |
| 644 | + | if (recipients.length == 1 && to == null) { | |
| 650 | 645 | attrs.set("to", recipients[0].asString()); | |
| 651 | 646 | } | |
| 652 | 647 |
+11
-0
borogove/Util.hx
#
| ... | ... | @@ -121,6 +122,16 @@ function xmlEscape(s: String) { | |
| 121 | 122 | return StringTools.replace(StringTools.replace(StringTools.replace(s, "&", "&"), "<", "<"), ">", ">"); | |
| 122 | 123 | } | |
| 123 | 124 | ||
| 125 | + | function addExtendedAddressing(stanza: Stanza, recipients: ReadOnlyArray<JID>) { | |
| 126 | + | if (recipients.length < 2) return stanza; | |
| 127 | + | final addresses = stanza.tag("addresses", { xmlns: "http://jabber.org/protocol/address" }); | |
| 128 | + | for (recipient in recipients) { | |
| 129 | + | addresses.tag("address", { type: "to", jid: recipient.asString(), delivered: "true" }).up(); | |
| 130 | + | } | |
| 131 | + | addresses.up(); | |
| 132 | + | return stanza; | |
| 133 | + | } | |
| 134 | + | ||
| 124 | 135 | // Taken from StringTools but modified not to decode + as space | |
| 125 | 136 | #if (flash || js || cs || python) inline #end function uriDecode(s:String):String { | |
| 126 | 137 | #if flash |
+64
-0
test/TestChat.hx
#
| ... | ... | @@ -1328,6 +1328,70 @@ class TestChat extends utest.Test { | |
| 1328 | 1328 | }); | |
| 1329 | 1329 | }); | |
| 1330 | 1330 | } | |
| 1331 | + | ||
| 1332 | + | public function testRemoveReactionExtendedMultiDMAddsAddressing(async: Async) { | |
| 1333 | + | final persistence = new RecordingPersistence(); | |
| 1334 | + | final client = new Client("me@example.com", persistence); | |
| 1335 | + | final chat = client.getDirectChat("bob@example.com\ncarol@example.com"); | |
| 1336 | + | final sent: Array<Stanza> = []; | |
| 1337 | + | ||
| 1338 | + | client.stream.on("sendStanza", (stanza: Stanza) -> { | |
| 1339 | + | if (stanza.name != "message" || stanza.getChild("reactions", "urn:xmpp:reactions:0") == null) return EventUnhandled; | |
| 1340 | + | sent.push(stanza); | |
| 1341 | + | if (sent.length < 2) return EventUnhandled; | |
| 1342 | + | ||
| 1343 | + | final tos = sent.map(s -> s.attr.get("to")); | |
| 1344 | + | tos.sort(Reflect.compare); | |
| 1345 | + | Assert.same(["bob@example.com", "carol@example.com"], tos); | |
| 1346 | + | for (s in sent) { | |
| 1347 | + | final addresses = s.getChild("addresses", "http://jabber.org/protocol/address"); | |
| 1348 | + | Assert.notNull(addresses); | |
| 1349 | + | if (addresses != null) { | |
| 1350 | + | final jids = addresses.allTags("address").map(a -> a.attr.get("jid")); | |
| 1351 | + | jids.sort(Reflect.compare); | |
| 1352 | + | Assert.same(["bob@example.com", "carol@example.com"], jids); | |
| 1353 | + | } | |
| 1354 | + | } | |
| 1355 | + | async.done(); | |
| 1356 | + | return EventHandled; | |
| 1357 | + | }); | |
| 1358 | + | ||
| 1359 | + | chat.removeReaction(reactionTarget(), borogove.Reaction.unicode("👍")); | |
| 1360 | + | } | |
| 1361 | + | ||
| 1362 | + | public function testRemoveReactionCompatMultiDMAddsAddressing(async: Async) { | |
| 1363 | + | final persistence = new RecordingPersistence(); | |
| 1364 | + | final client = new Client("me@example.com", persistence); | |
| 1365 | + | final routing = "multi@example.com"; | |
| 1366 | + | final chat = new borogove.Chat.DirectChat(client, client.stream, persistence, routing); | |
| 1367 | + | chat._fullCounterparts = ["bob@example.com", "carol@example.com"]; | |
| 1368 | + | ||
| 1369 | + | client.stream.on("sendStanza", (stanza: Stanza) -> { | |
| 1370 | + | if (stanza.name != "message" || stanza.getChild("reactions", "urn:xmpp:reactions:0") == null) return EventUnhandled; | |
| 1371 | + | Assert.equals(routing, stanza.attr.get("to")); | |
| 1372 | + | final addresses = stanza.getChild("addresses", "http://jabber.org/protocol/address"); | |
| 1373 | + | Assert.notNull(addresses); | |
| 1374 | + | if (addresses != null) { | |
| 1375 | + | Assert.same(["bob@example.com", "carol@example.com"], addresses.allTags("address").map(a -> a.attr.get("jid"))); | |
| 1376 | + | } | |
| 1377 | + | async.done(); | |
| 1378 | + | return EventHandled; | |
| 1379 | + | }); | |
| 1380 | + | ||
| 1381 | + | chat.removeReaction(reactionTarget(), borogove.Reaction.unicode("👍")); | |
| 1382 | + | } | |
| 1383 | + | ||
| 1384 | + | private function reactionTarget():borogove.ChatMessage { | |
| 1385 | + | final builder = new ChatMessageBuilder(); | |
| 1386 | + | builder.localId = "m1"; | |
| 1387 | + | builder.sortId = "a0"; | |
| 1388 | + | builder.direction = MessageReceived; | |
| 1389 | + | builder.from = JID.parse("bob@example.com"); | |
| 1390 | + | builder.senderId = "bob@example.com"; | |
| 1391 | + | builder.to = JID.parse("me@example.com"); | |
| 1392 | + | builder.text = "hi"; | |
| 1393 | + | return builder.build(); | |
| 1394 | + | } | |
| 1331 | 1395 | } | |
| 1332 | 1396 | ||
| 1333 | 1397 | @:access(borogove) |