borogove

created pr with borogove/multi-dm.1 on 2026-11-09T19:47:55Z · by 9e834e0d
added borogove/multi-dm.2 on 2026-11-10T19:08:22Z · by 9e834e0d
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 -3
checkout specific patchset revision:
ssh pr.pico.sh pull borogove:multi-dm [rev] | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh borogove:multi-dm
add comment to patch request:
ssh pr.pico.sh comment borogove:multi-dm "lgtm!"
+1 -8 borogove/Chat.hx #
......@@ -1363,7 +1363,7 @@ class DirectChat extends Chat {
13631363 final update = new ReactionUpdate(ID.unique(), null, null, m.localId, m.chatId(), client.accountId(), Date.format(std.Date.now()), reactions, EmojiReactions);
13641364 final outboxItem = outbox.newItem();
13651365 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);
13671367 if (stored != null) client.notifyMessageHandlers(stored, ReactionEvent);
13681368 });
13691369 }
......@@ -1377,13 +1377,6 @@ class DirectChat extends Chat {
13771377 thenshim.PromiseTools.all(counters.map(counterpart -> {
13781378 final clone = stanza.clone();
13791379 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- }
13871380
13881381 // Default should try all preferences in order?
13891382 // Otherwise force to the one we were passed?
+2 -7 borogove/ChatMessage.hx #
......@@ -640,13 +640,8 @@ class ChatMessage {
640640 var stanza = new Stanza("message", attrs);
641641 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();
642642 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) {
650645 attrs.set("to", recipients[0].asString());
651646 }
652647
+11 -0 borogove/Util.hx #
......@@ -1,6 +1,7 @@
11 package borogove;
22
33 import haxe.io.Bytes;
4+import haxe.ds.ReadOnlyArray;
45 import thenshim.Promise;
56 #if js
67 import js.html.TextEncoder;
......@@ -121,6 +122,16 @@ function xmlEscape(s: String) {
121122 return StringTools.replace(StringTools.replace(StringTools.replace(s, "&", "&amp;"), "<", "&lt;"), ">", "&gt;");
122123 }
123124
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+
124135 // Taken from StringTools but modified not to decode + as space
125136 #if (flash || js || cs || python) inline #end function uriDecode(s:String):String {
126137 #if flash
+64 -0 test/TestChat.hx #
......@@ -1264,6 +1264,70 @@ class TestChat extends utest.Test {
12641264 });
12651265 });
12661266 }
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+ }
12671331 }
12681332
12691333 @:access(borogove)
Back to top