git » sdk » commit 9d708b4

Initial handler to subscribe to user state

author Stephen Paul Weber
2024-09-24 18:51:45 UTC
committer Stephen Paul Weber
2024-09-24 18:51:45 UTC
parent e8a4f64875b582af2f43cfb7773cd3f1ea1769b8

Initial handler to subscribe to user state

Is the user typing? Paused typing? Active in the chat but not typing?
Etc.

This API might also make sense to be used to presence changes generally
as well, in which case there would be no chatId/threadId attached?

Makefile +1 -0
npm/index.ts +1 -0
snikket/Chat.hx +10 -2
snikket/ChatMessage.hx +1 -1
snikket/Client.hx +29 -1
snikket/Message.hx +23 -10
snikket/MessageSync.hx +1 -1

diff --git a/Makefile b/Makefile
index 5036ce2..6946754 100644
--- a/Makefile
+++ b/Makefile
@@ -13,6 +13,7 @@ npm/snikket.js:
 	sed -i 's/snikket\.UiState/enums.UiState/g' npm/snikket.d.ts
 	sed -i 's/snikket\.MessageStatus/enums.MessageStatus/g' npm/snikket.d.ts
 	sed -i 's/snikket\.MessageDirection/enums.MessageDirection/g' npm/snikket.d.ts
+	sed -i 's/snikket\.UserState/enums.UserState/g' npm/snikket.d.ts
 	sed -i '1ivar exports = {};' npm/snikket.js
 	echo "export const snikket = exports.snikket;" >> npm/snikket.js
 
diff --git a/npm/index.ts b/npm/index.ts
index 5a964cb..a66f7d6 100644
--- a/npm/index.ts
+++ b/npm/index.ts
@@ -20,6 +20,7 @@ export import jingle = snikket.jingle;
 export import UiState = enums.UiState;
 export import MessageStatus = enums.MessageStatus;
 export import MessageDirection = enums.MessageDirection;
+export import UserState = enums.UserState;
 
 export namespace persistence {
 	 export import browser = browserp;
diff --git a/snikket/Chat.hx b/snikket/Chat.hx
index 9457452..5fa0926 100644
--- a/snikket/Chat.hx
+++ b/snikket/Chat.hx
@@ -23,6 +23,14 @@ enum abstract UiState(Int) {
 	var Closed; // Archived
 }
 
+enum abstract UserState(Int) {
+	var Gone;
+	var Inactive;
+	var Active;
+	var Composing;
+	var Paused;
+}
+
 #if cpp
 @:build(HaxeCBridge.expose())
 @:build(HaxeSwiftBridge.expose())
@@ -611,7 +619,7 @@ class DirectChat extends Chat {
 		if (typingTimer != null) typingTimer.stop();
 		client.chatActivity(this);
 		message = prepareOutgoingMessage(message);
-		final fromStanza = Message.fromStanza(message.asStanza(), client.jid);
+		final fromStanza = Message.fromStanza(message.asStanza(), client.jid).parsed;
 		switch (fromStanza) {
 			case ChatMessageStanza(_):
 				persistence.storeMessage(client.accountId(), message, (stored) -> {
@@ -1021,7 +1029,7 @@ class Channel extends Chat {
 		final stanza = message.asStanza();
 		// Fake from as it will look on reflection for storage purposes
 		stanza.attr.set("from", getFullJid().asString());
-		final fromStanza = Message.fromStanza(stanza, client.jid);
+		final fromStanza = Message.fromStanza(stanza, client.jid).parsed;
 		stanza.attr.set("from", client.jid.asString());
 		switch (fromStanza) {
 			case ChatMessageStanza(_):
diff --git a/snikket/ChatMessage.hx b/snikket/ChatMessage.hx
index 11e7112..03ee468 100644
--- a/snikket/ChatMessage.hx
+++ b/snikket/ChatMessage.hx
@@ -142,7 +142,7 @@ class ChatMessage {
 
 	@:allow(snikket)
 	private static function fromStanza(stanza:Stanza, localJid:JID):Null<ChatMessage> {
-		switch Message.fromStanza(stanza, localJid) {
+		switch Message.fromStanza(stanza, localJid).parsed {
 			case ChatMessageStanza(message):
 				return message;
 			default:
diff --git a/snikket/Client.hx b/snikket/Client.hx
index 6f7b0c7..a967bb3 100644
--- a/snikket/Client.hx
+++ b/snikket/Client.hx
@@ -44,6 +44,7 @@ class Client extends EventEmitter {
 	public var sendAvailable(null, default): Bool = true;
 	private var stream:GenericStream;
 	private var chatMessageHandlers: Array<(ChatMessage)->Void> = [];
+	private var chatStateHandlers: Array<(String,String,Null<String>,UserState)->Void> = [];
 	@:allow(snikket)
 	private var jid(default,null):JID;
 	private var chats: Array<Chat> = [];
@@ -196,7 +197,8 @@ class Client extends EventEmitter {
 				}
 			}
 
-			switch (Message.fromStanza(stanza, this.jid)) {
+			final message = Message.fromStanza(stanza, this.jid);
+			switch (message.parsed) {
 				case ChatMessageStanza(chatMessage):
 					var chat = getChat(chatMessage.chatId());
 					if (chat == null && stanza.attr.get("type") != "groupchat") chat = getDirectChat(chatMessage.chatId());
@@ -222,6 +224,23 @@ class Client extends EventEmitter {
 					// ignore
 			}
 
+			if (stanza.attr.get("type") != "error") {
+				final chatState = stanza.getChild(null, "http://jabber.org/protocol/chatstates");
+				final userState = switch (chatState?.name) {
+					case "active": UserState.Active;
+					case "inactive": UserState.Inactive;
+					case "gone": UserState.Gone;
+					case "composing": UserState.Composing;
+					case "paused": UserState.Paused;
+					default: null;
+				};
+				if (userState != null) {
+					for (handler in chatStateHandlers) {
+						handler(message.senderId, message.chatId, message.threadId, userState);
+					}
+				}
+			}
+
 			final pubsubEvent = PubsubEvent.fromStanza(stanza);
 			if (pubsubEvent != null && pubsubEvent.getFrom() != null && pubsubEvent.getNode() == "urn:xmpp:avatar:metadata" && pubsubEvent.getItems().length > 0) {
 				final item = pubsubEvent.getItems()[0];
@@ -857,6 +876,15 @@ class Client extends EventEmitter {
 		});
 	}
 
+	#if !cpp
+	// TODO: haxe cpp erases enum into int, so using it as a callback arg is hard
+	// could just use int in C bindings, or need to come up with a good strategy
+	// for the wrapper
+	public function addUserStateListener(handler: (String,String,Null<String>,UserState)->Void):Void {
+		chatStateHandlers.push(handler);
+	}
+	#end
+
 	/**
 		Event fired when a new ChatMessage comes in on any Chat
 		Also fires when status of a ChatMessage changes,
diff --git a/snikket/Message.hx b/snikket/Message.hx
index 66f66d2..1c7a079 100644
--- a/snikket/Message.hx
+++ b/snikket/Message.hx
@@ -23,8 +23,22 @@ enum MessageStanza {
 
 @:nullSafety(Strict)
 class Message {
-	public static function fromStanza(stanza:Stanza, localJid:JID, ?inputTimestamp: String):MessageStanza {
-		if (stanza.attr.get("type") == "error") return ErrorMessageStanza(stanza);
+	public final chatId: String;
+	public final senderId: String;
+	public final threadId: Null<String>;
+	public final parsed: MessageStanza;
+
+	private function new(chatId: String, senderId: String, threadId: Null<String>, parsed: MessageStanza) {
+		this.chatId = chatId;
+		this.senderId = senderId;
+		this.threadId = threadId;
+		this.parsed = parsed;
+	}
+
+	public static function fromStanza(stanza:Stanza, localJid:JID, ?inputTimestamp: String):Message {
+		final fromAttr = stanza.attr.get("from");
+		final from = fromAttr == null ? localJid.domain : fromAttr;
+		if (stanza.attr.get("type") == "error") return new Message(from, from, null, ErrorMessageStanza(stanza));
 
 		var msg = new ChatMessage();
 		final timestamp = stanza.findText("{urn:xmpp:delay}delay@stamp") ?? inputTimestamp ?? Date.format(std.Date.now());
@@ -35,8 +49,7 @@ class Message {
 		if (msg.text != null && (msg.lang == null || msg.lang == "")) {
 			msg.lang = stanza.getChild("body")?.attr.get("xml:lang");
 		}
-		final from = stanza.attr.get("from");
-		msg.from = from == null ? null : JID.parse(from);
+		msg.from = JID.parse(from);
 		msg.isGroupchat = stanza.attr.get("type") == "groupchat";
 		msg.sender = stanza.attr.get("type") == "groupchat" ? msg.from : msg.from?.asBare();
 		final localJidBare = localJid.asBare();
@@ -97,7 +110,7 @@ class Message {
 					replyTo.clear();
 				} else if (jid == null) {
 					trace("No support for addressing to non-jid", address);
-					return UnknownMessageStanza(stanza);
+					return new Message(msg.chatId(), msg.senderId(), msg.threadId, UnknownMessageStanza(stanza));
 				} else if (address.attr.get("type") == "to" || address.attr.get("type") == "cc") {
 					recipients[JID.parse(jid).asBare().asString()] = true;
 					if (!anyExtendedReplyTo) replyTo[JID.parse(jid).asString()] = true; // reply all
@@ -124,7 +137,7 @@ class Message {
 		final msgFrom = msg.from;
 		if (msg.direction == MessageReceived && msgFrom != null && msg.replyTo.find((r) -> r.asBare().equals(msgFrom.asBare())) == null) {
 			trace("Don't know what chat message without from in replyTo belongs in", stanza);
-			return UnknownMessageStanza(stanza);
+			return new Message(msg.chatId(), msg.senderId(), msg.threadId, UnknownMessageStanza(stanza));
 		}
 
 		final reactionsEl = stanza.getChild("reactions", "urn:xmpp:reactions:0");
@@ -133,7 +146,7 @@ class Message {
 			final reactions = reactionsEl.allTags("reaction").map((r) -> r.getText());
 			final reactionId = reactionsEl.attr.get("id");
 			if (reactionId != null) {
-				return ReactionUpdateStanza(new ReactionUpdate(
+				return new Message(msg.chatId(), msg.senderId(), msg.threadId, ReactionUpdateStanza(new ReactionUpdate(
 					stanza.attr.get("id") ?? ID.long(),
 					stanza.attr.get("type") == "groupchat" ? reactionId : null,
 					stanza.attr.get("type") != "groupchat" ? reactionId : null,
@@ -141,7 +154,7 @@ class Message {
 					timestamp,
 					msg.senderId(),
 					reactions
-				));
+				)));
 			}
 		}
 
@@ -156,7 +169,7 @@ class Message {
 			msg.attachSims(sims);
 		}
 
-		if (msg.text == null && msg.attachments.length < 1) return UnknownMessageStanza(stanza);
+		if (msg.text == null && msg.attachments.length < 1) return new Message(msg.chatId(), msg.senderId(), msg.threadId, UnknownMessageStanza(stanza));
 
 		for (fallback in stanza.allTags("fallback", "urn:xmpp:fallback:0")) {
 			msg.payloads.push(fallback);
@@ -192,6 +205,6 @@ class Message {
 			Reflect.setField(msg, "localId", replaceId);
 		}
 
-		return ChatMessageStanza(msg);
+		return new Message(msg.chatId(), msg.senderId(), msg.threadId, ChatMessageStanza(msg));
 	}
 }
diff --git a/snikket/MessageSync.hx b/snikket/MessageSync.hx
index e834268..dde65c5 100644
--- a/snikket/MessageSync.hx
+++ b/snikket/MessageSync.hx
@@ -80,7 +80,7 @@ class MessageSync {
 				jmi.set(jmiChildren[0].attr.get("id"), originalMessage);
 			}
 
-			var msg = Message.fromStanza(originalMessage, client.jid, timestamp);
+			final msg = Message.fromStanza(originalMessage, client.jid, timestamp).parsed;
 
 			switch (msg) {
 				case ChatMessageStanza(chatMessage):