git » sdk » commit 3d77d20

Support for /me

author Stephen Paul Weber
2025-10-27 16:39:28 UTC
committer Stephen Paul Weber
2025-10-27 16:39:28 UTC
parent faec03436e328f87de189c1a3c7e2ae979322780

Support for /me

borogove/ChatMessage.hx +36 -17
borogove/Stanza.hx +6 -0

diff --git a/borogove/ChatMessage.hx b/borogove/ChatMessage.hx
index 946cc7f..a2deabb 100644
--- a/borogove/ChatMessage.hx
+++ b/borogove/ChatMessage.hx
@@ -333,11 +333,15 @@ class ChatMessage {
 		Get HTML version of the message body
 
 		WARNING: this is possibly untrusted HTML. You must parse or sanitize appropriately!
+
+		@param sender optionally specify the full details of the sender
 	**/
-	public function html():String {
+	public function html(sender: Null<Participant> = null):String {
 		final htmlBody = payloads.find((p) -> p.attr.get("xmlns") == "http://jabber.org/protocol/xhtml-im" && p.name == "html")?.getChild("body", "http://www.w3.org/1999/xhtml");
+		var htmlSource = "";
+		var isAction = false;
 		if (htmlBody != null) {
-			return htmlBody.getChildren().map(el -> el.traverse(child -> {
+			htmlSource = htmlBody.getChildren().map(el -> el.traverse(child -> {
 				if (child.name == "img") {
 					final src = child.attr.get("src");
 					if (src != null) {
@@ -348,24 +352,39 @@ class ChatMessage {
 					}
 					return true;
 				}
+				final senderP = sender;
+				if (senderP != null && child.getFirstChild() == null) {
+					final txt = child.getText();
+					if (txt.startsWith("/me")) {
+						isAction = true;
+						child.removeChildren();
+						child.text(senderP.displayName + txt.substr(3));
+					}
+				}
 				return false;
 			}).serialize()).join("");
+		} else {
+			var bodyText = text ?? "";
+			if (sender != null && bodyText.startsWith("/me")) {
+				isAction = true;
+				bodyText = sender.displayName + bodyText.substr(3);
+			}
+			final codepoints = StringUtil.codepointArray(bodyText);
+			// TODO: not every app will implement every feature. How should the app tell us what fallbacks to handle?
+			final fallbacks: Array<{start: Int, end: Int}> = cast payloads.filter(
+				(p) -> p.attr.get("xmlns") == "urn:xmpp:fallback:0" &&
+					(((p.attr.get("for") == "jabber:x:oob" || p.attr.get("for") == "urn:xmpp:sims:1") && attachments.length > 0) ||
+					 (replyToMessage != null && p.attr.get("for") == "urn:xmpp:reply:0") ||
+					 p.attr.get("for") == "http://jabber.org/protocol/address")
+			).map((p) -> p.getChild("body")).map((b) -> b == null ? null : { start: Std.parseInt(b.attr.get("start") ?? "0") ?? 0, end: Std.parseInt(b.attr.get("end") ?? Std.string(codepoints.length)) ?? codepoints.length }).filter((b) -> b != null);
+			fallbacks.sort((x, y) -> y.start - x.start);
+			for (fallback in fallbacks) {
+				codepoints.splice(fallback.start, (fallback.end - fallback.start));
+			}
+			final body = codepoints.join("");
+			htmlSource = payloads.find((p) -> p.attr.get("xmlns") == "urn:xmpp:styling:0" && p.name == "unstyled") == null ? XEP0393.parse(body).map((s) -> s.toString()).join("") : StringTools.htmlEscape(body);
 		}
-
-		final codepoints = StringUtil.codepointArray(text ?? "");
-		// TODO: not every app will implement every feature. How should the app tell us what fallbacks to handle?
-		final fallbacks: Array<{start: Int, end: Int}> = cast payloads.filter(
-			(p) -> p.attr.get("xmlns") == "urn:xmpp:fallback:0" &&
-				(((p.attr.get("for") == "jabber:x:oob" || p.attr.get("for") == "urn:xmpp:sims:1") && attachments.length > 0) ||
-				 (replyToMessage != null && p.attr.get("for") == "urn:xmpp:reply:0") ||
-				 p.attr.get("for") == "http://jabber.org/protocol/address")
-		).map((p) -> p.getChild("body")).map((b) -> b == null ? null : { start: Std.parseInt(b.attr.get("start") ?? "0") ?? 0, end: Std.parseInt(b.attr.get("end") ?? Std.string(codepoints.length)) ?? codepoints.length }).filter((b) -> b != null);
-		fallbacks.sort((x, y) -> y.start - x.start);
-		for (fallback in fallbacks) {
-			codepoints.splice(fallback.start, (fallback.end - fallback.start));
-		}
-		final body = codepoints.join("");
-		return payloads.find((p) -> p.attr.get("xmlns") == "urn:xmpp:styling:0" && p.name == "unstyled") == null ? XEP0393.parse(body).map((s) -> s.toString()).join("") : StringTools.htmlEscape(body);
+		return isAction ? '<div class="action">${htmlSource}</div>' : htmlSource;
 	}
 
 	/**
diff --git a/borogove/Stanza.hx b/borogove/Stanza.hx
index d5bc49a..702898b 100644
--- a/borogove/Stanza.hx
+++ b/borogove/Stanza.hx
@@ -408,6 +408,12 @@ class Stanza implements NodeInterface {
 
 	public function removeChildren(?name: String, ?xmlns_:String):Void {
 		serialized = null;
+
+		if (name == null && xmlns_ == null) {
+			children = [];
+			return;
+		}
+
 		final xmlns = xmlns_??attr.get("xmlns");
 		children = children.filter((child:Node) -> {
 			switch(child) {