| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2023-11-01 15:57:33 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2023-11-08 21:16:35 UTC |
| parent | 305477dad3cd470edad1b80a9beb4fc7a25f7f19 |
| xmpp/Chat.hx | +91 | -0 |
| xmpp/Client.hx | +47 | -22 |
| xmpp/Persistence.hx | +1 | -0 |
| xmpp/persistence/browser.js | +41 | -0 |
diff --git a/xmpp/Chat.hx b/xmpp/Chat.hx index c14ffe1..1593ce5 100644 --- a/xmpp/Chat.hx +++ b/xmpp/Chat.hx @@ -31,6 +31,8 @@ abstract class Chat { private var displayName:String; public var uiState = Open; private var extensions: Stanza; + private var _unreadCount = 0; + private var lastMessage: Null<ChatMessage>; public function new(client:Client, stream:GenericStream, persistence:Persistence, chatId:String, uiState = Open, extensions: Null<Stanza> = null) { this.client = client; @@ -56,6 +58,14 @@ abstract class Chat { abstract public function close():Void; + abstract public function markReadUpTo(message: ChatMessage):Void; + + abstract public function lastMessageId():Null<String>; + + public function lastMessageTimestamp():Null<String> { + return lastMessage?.timestamp; + } + public function updateFromBookmark(item: Stanza) { final conf = item.getChild("conference", "urn:xmpp:bookmarks:1"); final fn = conf.attr.get("name"); @@ -68,6 +78,27 @@ abstract class Chat { callback(Color.defaultPhoto(chatId, getDisplayName().charAt(0))); } + public function readUpTo() { + final displayed = extensions.getChild("displayed", "urn:xmpp:chat-markers:0"); + return displayed?.attr?.get("id"); + } + + public function unreadCount() { + return _unreadCount; + } + + public function setUnreadCount(count:Int) { + _unreadCount = count; + } + + public function preview() { + return lastMessage?.text ?? ""; + } + + public function setLastMessage(message:Null<ChatMessage>) { + lastMessage = message; + } + public function setDisplayName(fn:String) { this.displayName = fn; } @@ -242,6 +273,37 @@ class DirectChat extends Chat { message.to = recipient; client.sendStanza(message.asStanza()); } + setLastMessage(message); + client.trigger("chats/update", [this]); + } + + public function lastMessageId() { + return lastMessage?.localId ?? lastMessage?.serverId; + } + + public function markReadUpTo(message: ChatMessage) { + if (readUpTo() == message.localId || readUpTo() == message.serverId) return; + final upTo = message.localId ?? message.serverId; + _unreadCount = 0; // TODO + for (recipient in getParticipants()) { + // TODO: extended addressing when relevant + final stanza = new Stanza("message", { to: recipient, id: ID.long() }) + .tag("displayed", { xmlns: "urn:xmpp:chat-markers:0", id: upTo }).up(); + if (message.threadId != null) { + stanza.textTag("thread", message.threadId); + } + client.sendStanza(stanza); + } + + var displayed = extensions.getChild("displayed", "urn:xmpp:chat-markers:0"); + if (displayed == null) { + displayed = new Stanza("displayed", { xmlns: "urn:xmpp:chat-markers:0", id: upTo }); + extensions.addChild(displayed); + } else { + displayed.attr.set("id", upTo); + } + persistence.storeChat(client.accountId(), this); + client.trigger("chats/update", [this]); } public function bookmark() { @@ -406,6 +468,35 @@ class Channel extends Chat { message.recipients = [message.to]; persistence.storeMessage(client.accountId(), message); client.sendStanza(message.asStanza("groupchat")); + setLastMessage(message); + client.trigger("chats/update", [this]); + } + + public function lastMessageId() { + return lastMessage?.serverId; + } + + public function markReadUpTo(message: ChatMessage) { + if (readUpTo() == message.serverId) return; + final upTo = message.serverId; + _unreadCount = 0; + final stanza = new Stanza("message", { to: chatId, id: ID.long() }) + .tag("displayed", { xmlns: "urn:xmpp:chat-markers:0", id: upTo }).up(); + if (message.threadId != null) { + stanza.textTag("thread", message.threadId); + } + client.sendStanza(stanza); + + var displayed = extensions.getChild("displayed", "urn:xmpp:chat-markers:0"); + if (displayed == null) { + displayed = new Stanza("displayed", { xmlns: "urn:xmpp:chat-markers:0", id: upTo }); + extensions.addChild(displayed); + } else { + displayed.attr.set("id", upTo); + } + persistence.storeChat(client.accountId(), this); + bookmark(); // TODO: what if not previously bookmarked? + client.trigger("chats/update", [this]); } public function bookmark() { diff --git a/xmpp/Client.hx b/xmpp/Client.hx index c16a852..0b8388f 100644 --- a/xmpp/Client.hx +++ b/xmpp/Client.hx @@ -65,15 +65,25 @@ class Client extends xmpp.EventEmitter { for (protoChat in protoChats) { chats.push(protoChat.toChat(this, stream, persistence)); } - this.trigger("chats/update", chats); - - persistence.getLogin(jid, (login) -> { - if (login.token == null) { - stream.on("auth/password-needed", (data)->this.trigger("auth/password-needed", { jid: this.jid })); - } else { - stream.on("auth/password-needed", (data)->this.stream.trigger("auth/password", { password: login.token })); + persistence.getChatsUnreadDetails(accountId(), chats, (details) -> { + for (detail in details) { + var chat = getChat(detail.chatId); + if (chat != null) { + chat.setLastMessage(detail.message); + chat.setUnreadCount(detail.unreadCount); + } } - stream.connect(login.clientId == null ? jid : jid + "/" + login.clientId); + chats.sort((a, b) -> -Reflect.compare(a.lastMessageTimestamp() ?? "0", b.lastMessageTimestamp() ?? "0")); + this.trigger("chats/update", chats); + + persistence.getLogin(jid, (login) -> { + if (login.token == null) { + stream.on("auth/password-needed", (data)->this.trigger("auth/password-needed", { jid: this.jid })); + } else { + stream.on("auth/password-needed", (data)->this.stream.trigger("auth/password", { password: login.token })); + } + stream.connect(login.clientId == null ? jid : jid + "/" + login.clientId); + }); }); }); } @@ -140,8 +150,11 @@ class Client extends xmpp.EventEmitter { var chat = getChat(chatMessage.chatId()); if (chat == null && stanza.attr.get("type") != "groupchat") chat = getDirectChat(chatMessage.chatId()); if (chat != null) { - chatActivity(chat); chatMessage = chat.prepareIncomingMessage(chatMessage, stanza); + chat.setLastMessage(chatMessage); + chat.setUnreadCount(chatMessage.isIncoming() ? chat.unreadCount() + 1 : 0); + if (chatMessage.serverId != null) persistence.storeMessage(accountId(), chatMessage); + chatActivity(chat); for (handler in chatMessageHandlers) { handler(chatMessage); } @@ -315,18 +328,32 @@ class Client extends xmpp.EventEmitter { }); // Enable carbons - stream.sendStanza( + sendStanza( new Stanza("iq", { type: "set", id: ID.short() }) .tag("enable", { xmlns: "urn:xmpp:carbons:2" }) .up() ); rosterGet(); - bookmarksGet(); - sync(() -> { - // Set self to online - sendPresence(); - this.trigger("status/online", {}); + bookmarksGet(() -> { + sync(() -> { + persistence.getChatsUnreadDetails(accountId(), chats, (details) -> { + for (detail in details) { + var chat = getChat(detail.chatId) ?? getDirectChat(detail.chatId, false); + final initialLastId = chat.lastMessageId(); + chat.setLastMessage(detail.message); + chat.setUnreadCount(detail.unreadCount); + if (detail.unreadCount > 0 && initialLastId != chat.lastMessageId()) { + chatActivity(chat, false); + } + } + chats.sort((a, b) -> -Reflect.compare(a.lastMessageTimestamp() ?? "0", b.lastMessageTimestamp() ?? "0")); + this.trigger("chats/update", chats); + // Set self to online + sendPresence(); + this.trigger("status/online", {}); + }); + }); }); return EventHandled; @@ -444,7 +471,7 @@ class Client extends xmpp.EventEmitter { } } - public function chatActivity(chat: Chat) { + public function chatActivity(chat: Chat, trigger = true) { if (chat.uiState == Closed) { chat.uiState = Open; persistence.storeChat(accountId(), chat); @@ -453,7 +480,7 @@ class Client extends xmpp.EventEmitter { if (idx > 0) { chats.splice(idx, 1); chats.unshift(chat); - this.trigger("chats/update", [chat]); + if (trigger) this.trigger("chats/update", [chat]); } } @@ -545,7 +572,8 @@ class Client extends xmpp.EventEmitter { sendQuery(rosterGet); } - private function bookmarksGet() { + // This is called right before we're going to trigger for all chats anyway, so don't bother with single triggers + private function bookmarksGet(callback: ()->Void) { final pubsubGet = new PubsubGet(null, "urn:xmpp:bookmarks:1"); pubsubGet.onFinished(() -> { for (item in pubsubGet.getResult()) { @@ -561,7 +589,6 @@ class Client extends xmpp.EventEmitter { final chat = getDirectChat(item.attr.get("id"), false); chat.updateFromBookmark(item); persistence.storeChat(accountId(), chat); - this.trigger("chats/update", [chat]); } } else { persistence.storeCaps(resultCaps); @@ -576,12 +603,10 @@ class Client extends xmpp.EventEmitter { chat.updateFromBookmark(item); chats.unshift(chat); persistence.storeChat(accountId(), chat); - this.trigger("chats/update", [chat]); } else { final chat = getDirectChat(item.attr.get("id"), false); chat.updateFromBookmark(item); persistence.storeChat(accountId(), chat); - this.trigger("chats/update", [chat]); } } }); @@ -589,10 +614,10 @@ class Client extends xmpp.EventEmitter { } else { chat.updateFromBookmark(item); persistence.storeChat(accountId(), chat); - this.trigger("chats/update", [chat]); } } } + callback(); }); sendQuery(pubsubGet); } diff --git a/xmpp/Persistence.hx b/xmpp/Persistence.hx index 3ef9b06..8dc9d14 100644 --- a/xmpp/Persistence.hx +++ b/xmpp/Persistence.hx @@ -8,6 +8,7 @@ abstract class Persistence { abstract public function lastId(accountId: String, chatId: Null<String>, callback:(serverId:Null<String>)->Void):Void; abstract public function storeChat(accountId: String, chat: Chat):Void; abstract public function getChats(accountId: String, callback: (chats:Array<SerializedChat>)->Void):Void; + abstract public function getChatsUnreadDetails(accountId: String, chats: Array<Chat>, callback: (details:Array<{ chatId: String, message: ChatMessage, unreadCount: Int }>)->Void):Void; abstract public function storeMessage(accountId: String, message: ChatMessage):Void; abstract public function getMessages(accountId: String, chatId: String, beforeId: Null<String>, beforeTime: Null<String>, callback: (messages:Array<ChatMessage>)->Void):Void; abstract public function getMediaUri(hashAlgorithm:String, hash:BytesData, callback: (uri:Null<String>)->Void):Void; diff --git a/xmpp/persistence/browser.js b/xmpp/persistence/browser.js index 90bd6e9..d26443b 100644 --- a/xmpp/persistence/browser.js +++ b/xmpp/persistence/browser.js @@ -129,6 +129,47 @@ exports.xmpp.persistence = { )))); }, + getChatsUnreadDetails: function(account, chatsArray, callback) { + const tx = db.transaction(["messages"], "readonly"); + const store = tx.objectStore("messages"); + + const cursor = store.index("chats").openCursor( + IDBKeyRange.bound([account], [account, [], []]), + "prev" + ); + const chats = {}; + chatsArray.forEach((chat) => chats[chat.chatId] = chat); + const result = {}; + var rowCount = 0; + cursor.onsuccess = (event) => { + if (event.target.result && rowCount < 1000) { + rowCount++; + const value = event.target.result.value; + if (result[value.chatId]) { + if (!result[value.chatId].foundAll) { + const readUpTo = chats[value.chatId]?.readUpTo(); + if (readUpTo === value.serverId || readUpTo === value.localId || value.direction == "MessageSent") { + result[value.chatId].foundAll = true; + } else { + result[value.chatId].unreadCount++; + } + } + } else { + const readUpTo = chats[value.chatId]?.readUpTo(); + const haveRead = readUpTo === value.serverId || readUpTo === value.localId || value.direction == "MessageSent"; + result[value.chatId] = { chatId: value.chatId, message: hydrateMessage(value), unreadCount: haveRead ? 0 : 1, foundAll: haveRead }; + } + event.target.result.continue(); + } else { + callback(Object.values(result)); + } + } + cursor.onerror = (event) => { + console.error(event); + callback([]); + } + }, + storeMessage: function(account, message) { const tx = db.transaction(["messages"], "readwrite"); const store = tx.objectStore("messages");