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.1
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
2026-11-09T19:40:59Z
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
#
| ... | ... | @@ -1363,7 +1363,7 @@ class DirectChat extends Chat { | |
| 1363 | 1363 | final update = new ReactionUpdate(ID.unique(), null, null, m.localId, m.chatId(), client.accountId(), Date.format(std.Date.now()), reactions, EmojiReactions); | |
| 1364 | 1364 | final outboxItem = outbox.newItem(); | |
| 1365 | 1365 | persistence.storeReaction(client.accountId(), update).then((stored) -> { | |
| 1366 | - | sendMessageStanza(update.asStanza(), outboxItem); | |
| 1366 | + | sendMessageStanza(Util.addExtendedAddressing(update.asStanza(), _fullCounterparts.map((p) -> JID.parse(p))), outboxItem); | |
| 1367 | 1367 | if (stored != null) client.notifyMessageHandlers(stored, ReactionEvent); | |
| 1368 | 1368 | }); | |
| 1369 | 1369 | } |
| ... | ... | @@ -1377,13 +1377,6 @@ class DirectChat extends Chat { | |
| 1377 | 1377 | thenshim.PromiseTools.all(counters.map(counterpart -> { | |
| 1378 | 1378 | final clone = stanza.clone(); | |
| 1379 | 1379 | clone.attr.set("to", counterpart); | |
| 1380 | - | if (counters.length > 1 && stanza.getChild("addresses", "http://jabber.org/protocol/address") == null) { | |
| 1381 | - | final addresses = clone.tag("addresses", { xmlns: "http://jabber.org/protocol/address" }); | |
| 1382 | - | for (counter in counters) { | |
| 1383 | - | addresses.tag("address", { type: "to", jid: counter, delivered: "true" }).up(); | |
| 1384 | - | } | |
| 1385 | - | addresses.up(); | |
| 1386 | - | } | |
| 1387 | 1380 | ||
| 1388 | 1381 | // Default should try all preferences in order? | |
| 1389 | 1382 | // 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
#
| ... | ... | @@ -1264,6 +1264,70 @@ class TestChat extends utest.Test { | |
| 1264 | 1264 | }); | |
| 1265 | 1265 | }); | |
| 1266 | 1266 | } | |
| 1267 | + | ||
| 1268 | + | public function testRemoveReactionExtendedMultiDMAddsAddressing(async: Async) { | |
| 1269 | + | final persistence = new RecordingPersistence(); | |
| 1270 | + | final client = new Client("me@example.com", persistence); | |
| 1271 | + | final chat = client.getDirectChat("bob@example.com\ncarol@example.com"); | |
| 1272 | + | final sent: Array<Stanza> = []; | |
| 1273 | + | ||
| 1274 | + | client.stream.on("sendStanza", (stanza: Stanza) -> { | |
| 1275 | + | if (stanza.name != "message" || stanza.getChild("reactions", "urn:xmpp:reactions:0") == null) return EventUnhandled; | |
| 1276 | + | sent.push(stanza); | |
| 1277 | + | if (sent.length < 2) return EventUnhandled; | |
| 1278 | + | ||
| 1279 | + | final tos = sent.map(s -> s.attr.get("to")); | |
| 1280 | + | tos.sort(Reflect.compare); | |
| 1281 | + | Assert.same(["bob@example.com", "carol@example.com"], tos); | |
| 1282 | + | for (s in sent) { | |
| 1283 | + | final addresses = s.getChild("addresses", "http://jabber.org/protocol/address"); | |
| 1284 | + | Assert.notNull(addresses); | |
| 1285 | + | if (addresses != null) { | |
| 1286 | + | final jids = addresses.allTags("address").map(a -> a.attr.get("jid")); | |
| 1287 | + | jids.sort(Reflect.compare); | |
| 1288 | + | Assert.same(["bob@example.com", "carol@example.com"], jids); | |
| 1289 | + | } | |
| 1290 | + | } | |
| 1291 | + | async.done(); | |
| 1292 | + | return EventHandled; | |
| 1293 | + | }); | |
| 1294 | + | ||
| 1295 | + | chat.removeReaction(reactionTarget(), borogove.Reaction.unicode("👍")); | |
| 1296 | + | } | |
| 1297 | + | ||
| 1298 | + | public function testRemoveReactionCompatMultiDMAddsAddressing(async: Async) { | |
| 1299 | + | final persistence = new RecordingPersistence(); | |
| 1300 | + | final client = new Client("me@example.com", persistence); | |
| 1301 | + | final routing = "multi@example.com"; | |
| 1302 | + | final chat = new borogove.Chat.DirectChat(client, client.stream, persistence, routing); | |
| 1303 | + | chat._fullCounterparts = ["bob@example.com", "carol@example.com"]; | |
| 1304 | + | ||
| 1305 | + | client.stream.on("sendStanza", (stanza: Stanza) -> { | |
| 1306 | + | if (stanza.name != "message" || stanza.getChild("reactions", "urn:xmpp:reactions:0") == null) return EventUnhandled; | |
| 1307 | + | Assert.equals(routing, stanza.attr.get("to")); | |
| 1308 | + | final addresses = stanza.getChild("addresses", "http://jabber.org/protocol/address"); | |
| 1309 | + | Assert.notNull(addresses); | |
| 1310 | + | if (addresses != null) { | |
| 1311 | + | Assert.same(["bob@example.com", "carol@example.com"], addresses.allTags("address").map(a -> a.attr.get("jid"))); | |
| 1312 | + | } | |
| 1313 | + | async.done(); | |
| 1314 | + | return EventHandled; | |
| 1315 | + | }); | |
| 1316 | + | ||
| 1317 | + | chat.removeReaction(reactionTarget(), borogove.Reaction.unicode("👍")); | |
| 1318 | + | } | |
| 1319 | + | ||
| 1320 | + | private function reactionTarget():borogove.ChatMessage { | |
| 1321 | + | final builder = new ChatMessageBuilder(); | |
| 1322 | + | builder.localId = "m1"; | |
| 1323 | + | builder.sortId = "a0"; | |
| 1324 | + | builder.direction = MessageReceived; | |
| 1325 | + | builder.from = JID.parse("bob@example.com"); | |
| 1326 | + | builder.senderId = "bob@example.com"; | |
| 1327 | + | builder.to = JID.parse("me@example.com"); | |
| 1328 | + | builder.text = "hi"; | |
| 1329 | + | return builder.build(); | |
| 1330 | + | } | |
| 1267 | 1331 | } | |
| 1268 | 1332 | ||
| 1269 | 1333 | @:access(borogove) |