| author | Eric Roberts
<eric@devl.me> 2026-10-07 15:34:46 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-10-07 15:36:17 UTC |
| parent | 790849e47ccc9a6a6b04eba4a1433757103c54b2 |
| borogove/Chat.hx | +41 | -28 |
| borogove/Client.hx | +2 | -2 |
| borogove/MemberUpdate.hx | +1 | -1 |
| test/TestChat.hx | +174 | -19 |
| test/TestClient.hx | +3 | -1 |
| test/TestSqlite.hx | +25 | -13 |
| test/idb.spec.ts | +4 | -2 |
| test/persistence-tests.ts | +5 | -7 |
| test/sqlite.spec.ts | +4 | -2 |
diff --git a/borogove/Chat.hx b/borogove/Chat.hx index 6089ef4..aa149ad 100644 --- a/borogove/Chat.hx +++ b/borogove/Chat.hx @@ -1535,7 +1535,8 @@ class Channel extends Chat { private var sync: Null<MessageSync> = null; private var forceLive = false; @:allow(borogove) - private var self: Null<Member> = null; + private var self: Member; + private var isJoined(default, null): Bool = false; @:allow(borogove) private var mavUntil = null; @:allow(borogove.SerializedChat) @@ -1551,6 +1552,18 @@ class Channel extends Chat { "SYNC: channel inSync", { reason: "initialize", chatId: chatId, inSync: inSync } ); + final nick = client.displayName(); + final account = client.getDirectChat(client.accountId(), false); + self = new Member( + JID.parse(chatId).withResource(nick).asString(), + nick, + account.getPhoto(), + true, + [], + JID.parse(account.chatId), + new Map(), + new AvailableChat(account.chatId, account.getDisplayName(), account.chatId, CapsRepo.empty) + ); if (disco != null) { this.disco = disco; if (!disco.features.contains("http://jabber.org/protocol/muc")) { @@ -1612,8 +1625,7 @@ class Channel extends Chat { return (justCleared ? Promise.resolve(null) : persistence.clearMemberPresence(client.accountId(), chatId)).then(_ -> { joinFailed = null; - self = null; - outbox.pause(); + disconnected(); inSync = false; trace( "SYNC: channel inSync", @@ -1671,7 +1683,7 @@ class Channel extends Chat { Can the user set the Channel subject? **/ public function canSetSubject() { - if (self == null || disco == null) return false; + if (!isJoined || disco == null) return false; final it = self.presence.iterator(); if (!it.hasNext()) return false; @@ -1719,8 +1731,6 @@ class Channel extends Chat { override public function canInvite() { if (!isPrivate()) return true; - if (self == null) return false; - final it = self.presence.iterator(); if (!it.hasNext()) return false; @@ -1731,7 +1741,6 @@ class Channel extends Chat { override public function canSend() { if (!super.canSend()) return false; - if (self == null) return true; final it = self.presence.iterator(); if (!it.hasNext()) return true; @@ -1740,7 +1749,7 @@ class Channel extends Chat { } override public function canModerate() { - if (self == null || disco == null) return false; + if (!isJoined || disco == null) return false; final it = self.presence.iterator(); if (!it.hasNext()) return false; @@ -1764,8 +1773,6 @@ class Channel extends Chat { } private function isOwner() { - if (self == null) return false; - return self.roles.exists(r -> r.id == "owner"); } @@ -1869,12 +1876,9 @@ class Channel extends Chat { } if (member.isSelf) { if (presence.type == "unavailable") { - self = null; - outbox.pause(); + disconnected(); } else { - self = member; - outbox.start(); - client.channelPinger.schedule(this); + joined(member); } if (!noStore) client.trigger("chats/update", [this]); } @@ -2102,11 +2106,24 @@ class Channel extends Chat { return lastMessageSenderName + ": " + super.preview(); } + private function joined(member: Member): Void { + self = member; + isJoined = true; + outbox.start(); + client.channelPinger.schedule(this); + } + + private function disconnected(): Void { + isJoined = false; + outbox.pause(); + client.channelPinger.remove(this); + } + @:allow(borogove) override private function livePresence() { if (forceLive) return true; - return self != null; + return isJoined; } override public function syncing() { @@ -2140,7 +2157,7 @@ class Channel extends Chat { } private function nickInUse() { - return self?.displayName ?? client.displayName(); + return self.displayName; } @:allow(borogove) @@ -2151,13 +2168,13 @@ class Channel extends Chat { @HaxeCBridge.noemit // on superclass as abstract public function members() { return persistence - .getMembers(client.accountId(), this, self == null ? false : self.roles.exists(r -> ["admin", "owner"].contains(r.id))) + .getMembers(client.accountId(), this, self.roles.exists(r -> ["admin", "owner"].contains(r.id))) .then(members -> members.filter(member -> member.id != chatId)); } @HaxeCBridge.noemit // on superclass as abstract public function getMemberDetails(memberIds: Array<String>) { - if (memberIds.length == 1 && self != null && memberIds[0] == self.id) return Promise.resolve([cast self]); + if (memberIds.length == 1 && memberIds[0] == self.id) return Promise.resolve([cast self]); return persistence.getMemberDetails(client.accountId(), this, memberIds); } @@ -2212,7 +2229,7 @@ class Channel extends Chat { final affRole = Role.forAffiliation(presence.mucUser.affiliation); if (affRole != null) roles.unshift(affRole); } - if (oneTen != null || id == self?.id) { + if (oneTen != null || id == self.id) { final chat = client.getDirectChat(client.accountId(), false); return new Member( id, @@ -2240,8 +2257,6 @@ class Channel extends Chat { } override public function availableRoles(member: Member):Array<Role> { - if (self == null) return []; - if (self.roles.exists(r -> r.id == "owner")) { return ["owner", "admin", "none", "outcast"].filter(aff -> !member.roles.exists(r -> r.id == aff)).map(aff -> Role.forAffiliation(aff)); } @@ -2256,8 +2271,6 @@ class Channel extends Chat { } override public function canRemoveRole(member: Member, role: Role):Bool { - if (self == null) return false; - if (self.roles.exists(r -> r.id == "owner")) { return ["owner", "admin", "none", "outcast"].contains(role.id); } @@ -2377,7 +2390,7 @@ class Channel extends Chat { } final occupantId = stanza.getChild("occupant-id", "urn:xmpp:occupant-id:0")?.attr?.get("id"); message.senderId = occupantId == null ? stanza.attr.get("from") : chatId + "/" + occupantId; // MUC always needs full JIDs - if (message.senderId == self?.id) { + if (message.senderId == self.id) { message.recipients = message.replyTo; message.direction = MessageSent; } @@ -2390,7 +2403,7 @@ class Channel extends Chat { message.timeReceived = Date.format(std.Date.now()); message.direction = MessageSent; message.from = client.jid; - message.senderId = self?.id ?? getFullJid().asString(); // TODO: what if we aren't joined and self is null and this is an occupant id room? + message.senderId = self.id; message.replyTo = [getFullJid()]; message.to = JID.parse(chatId); message.recipients = [message.to]; @@ -2486,11 +2499,11 @@ class Channel extends Chat { final reactions = []; for (areaction => reacts in m.reactions) { if (areaction != reaction.key) { - final react = reacts.find(r -> r.senderId == self?.id); + final react = reacts.find(r -> r.senderId == self.id); if (react != null && !Std.isOfType(react, CustomEmojiReaction)) reactions.push(react); } } - final update = new ReactionUpdate(ID.unique(), m.serverId, m.chatId(), null, m.chatId(), self?.id, Date.format(std.Date.now()), reactions, EmojiReactions); + final update = new ReactionUpdate(ID.unique(), m.serverId, m.chatId(), null, m.chatId(), self.id, Date.format(std.Date.now()), reactions, EmojiReactions); final outboxItem = outbox.newItem(); persistence.storeReaction(client.accountId(), update).then((stored) -> { sendMessageStanza(update.asStanza(), outboxItem); diff --git a/borogove/Client.hx b/borogove/Client.hx index b69fb65..8197138 100644 --- a/borogove/Client.hx +++ b/borogove/Client.hx @@ -1101,14 +1101,14 @@ class Client extends EventEmitter { // We resumed from disk so these must have been synced before // and will get anything since live for (channel in getChannels()) { - channel.inSync = channel.self != null && channel.sortId != null; + channel.inSync = channel.livePresence() && channel.sortId != null; trace( "SYNC: channel inSync", { reason: "resume from disk", chatId: channel.chatId, inSync: channel.inSync, - isJoined: channel.self != null, + livePresence: channel.livePresence(), sortId: channel.sortId, } ); diff --git a/borogove/MemberUpdate.hx b/borogove/MemberUpdate.hx index 70d532b..1ab9560 100644 --- a/borogove/MemberUpdate.hx +++ b/borogove/MemberUpdate.hx @@ -80,7 +80,7 @@ class MemberUpdate { id, jid, item.attr.get("nick") ?? resource, - jid?.asString() == accountId || channel?.self?.id == id, + jid?.asString() == accountId || channel?.self.id == id, Role.forAffiliation(aff), stanza.name == "presence" ? [ resource ?? "" => stanza ] : new Map() )); diff --git a/test/TestChat.hx b/test/TestChat.hx index 615a8b6..71269cb 100644 --- a/test/TestChat.hx +++ b/test/TestChat.hx @@ -418,11 +418,11 @@ class TestChat extends utest.Test { final client = new Client("test@example.com", persistence); final chat = new borogove.Chat.Channel(client, client.stream, persistence, "channel@example.com"); - chat.self = new Member("me", "myself", null, true, [new Role("member", "")], JID.parse("test@example.com"), new Map(), null); + chat.joined(new Member("me", "myself", null, true, [new Role("member", "")], JID.parse("test@example.com"), new Map(), null)); chat.commands().then(cmds -> { Assert.equals(0, cmds.length); - chat.self = new Member("me", "myself", null, true, [new Role("owner", "")], JID.parse("test@example.com"), new Map(), null); + chat.joined(new Member("me", "myself", null, true, [new Role("owner", "")], JID.parse("test@example.com"), new Map(), null)); chat.commands().then(cmds2 -> { Assert.equals(1, cmds2.length); Assert.equals("Settings", cmds2[0].name); @@ -450,23 +450,23 @@ class TestChat extends utest.Test { chat.disco = new borogove.Caps("", [], ["urn:xmpp:message-moderate:1", "http://jabber.org/protocol/muc"], []); Assert.isFalse(chat.canModerate()); - chat.self = new Member("me", "myself", null, true, [], JID.parse("test@example.com"), new Map(), null); + chat.joined(new Member("me", "myself", null, true, [], JID.parse("test@example.com"), new Map(), null)); Assert.isFalse(chat.canModerate()); // Presence set but not moderator - chat.self = new Member( + chat.joined(new Member( "me", "myself", null, true, [], JID.parse("test@example.com"), ["myself" => new borogove.Presence(null, new Stanza("x", { xmlns: "http://jabber.org/protocol/muc#user" }).tag("item", { role: "participant" }).up(), null)], null - ); + )); Assert.isFalse(chat.canModerate()); // Is moderator - chat.self = new Member( + chat.joined(new Member( "me", "myself", null, true, [], JID.parse("test@example.com"), ["myself" => new borogove.Presence(null, new Stanza("x", { xmlns: "http://jabber.org/protocol/muc#user" }).tag("item", { role: "moderator" }).up(), null)], null - ); + )); Assert.isTrue(chat.canModerate()); } @@ -479,11 +479,11 @@ class TestChat extends utest.Test { Assert.isFalse(chat.canSetSubject()); // Self set but no disco - chat.self = new Member( + chat.joined(new Member( "me", "myself", null, true, [], JID.parse("test@example.com"), ["myself" => new borogove.Presence(null, new Stanza("x", { xmlns: "http://jabber.org/protocol/muc#user" }).tag("item", { role: "participant" }).up(), null)], null - ); + )); Assert.isFalse(chat.canSetSubject()); chat.disco = new borogove.Caps("", [], [], []); @@ -492,19 +492,19 @@ class TestChat extends utest.Test { Assert.isFalse(chat.canSetSubject()); // Is moderator, subjectmod not set - chat.self = new Member( + chat.joined(new Member( "me", "myself", null, true, [], JID.parse("test@example.com"), ["myself" => new borogove.Presence(null, new Stanza("x", { xmlns: "http://jabber.org/protocol/muc#user" }).tag("item", { role: "moderator" }).up(), null)], null - ); + )); Assert.isTrue(chat.canSetSubject()); // Is participant, subjectmod set - chat.self = new Member( + chat.joined(new Member( "me", "myself", null, true, [], JID.parse("test@example.com"), ["myself" => new borogove.Presence(null, new Stanza("x", { xmlns: "http://jabber.org/protocol/muc#user" }).tag("item", { role: "participant" }).up(), null)], null - ); + )); chat.disco = new borogove.Caps("", [], [], [new Stanza("x", { xmlns: "jabber:x:data", type: "result" }).tag("field", { "var": "FORM_TYPE", type: "hidden" }).textTag("value", "http://jabber.org/protocol/muc#roominfo").up().tag("field", { "var": "muc#roominfo_subjectmod" }).textTag("value", "1").up()]); Assert.isTrue(chat.canSetSubject()); } @@ -547,6 +547,110 @@ class TestChat extends utest.Test { chat.setSubject("New Thread Subject", "thread123"); } + public function testChannelPlaceholderSelf(async: Async) { + final persistence = new Dummy(); + final client = new Client("test@example.com", persistence); + final account = client.getDirectChat(client.accountId(), false); + final channel = new Channel(client, client.stream, persistence, "channel@example.com"); + final self = channel.self; + + Assert.equals("channel@example.com/" + client.displayName(), self.id); + Assert.equals(client.displayName(), self.displayName); + Assert.equals(account.getPhoto(), self.photoUri); + Assert.equals(client.accountId(), self.jid.asString()); + Assert.equals(account.chatId, self.chat.chatId); + Assert.equals(account.getDisplayName(), self.chat.displayName); + Assert.isTrue(self.isSelf); + Assert.same([], self.roles); + Assert.isFalse(self.presence.iterator().hasNext()); + Assert.isFalse(channel.isJoined); + Assert.isFalse(channel.livePresence()); + Assert.isFalse(channel.canSetSubject()); + Assert.isFalse(channel.canModerate()); + Assert.isFalse(channel.isOwner()); + Assert.same([], channel.availableRoles(self)); + channel.disco = new borogove.Caps("", [], ["http://jabber.org/protocol/muc", "muc_membersonly"], []); + Assert.isFalse(channel.canInvite()); + final outgoing = channel.prepareOutgoingMessage(new ChatMessageBuilder()); + Assert.equals(self.id, outgoing.senderId); + Assert.equals(self.id, outgoing.replyTo[0].asString()); + + channel.getMemberDetails([self.id]).then(members -> { + Assert.equals(1, members.length); + Assert.equals(self, members[0]); + async.done(); + }); + } + + public function testServerSelfReplacesPlaceholder() { + final persistence = new Dummy(); + final client = new Client("test@example.com", persistence); + final channel = new Channel(client, client.stream, persistence, "channel@example.com"); + final placeholder = channel.self; + final presence = new Stanza("presence", { xmlns: "jabber:client", from: "channel@example.com/server-nick", to: "test@example.com" }) + .tag("occupant-id", { xmlns: "urn:xmpp:occupant-id:0", id: "server-id" }).up() + .tag("x", { xmlns: "http://jabber.org/protocol/muc#user" }) + .tag("item", { affiliation: "owner", role: "moderator" }).up() + .tag("status", { code: "110" }).up() + .up(); + + channel.setPresence("server-nick", cast presence, false); + + Assert.isTrue(channel.isJoined); + Assert.isFalse(placeholder == channel.self); + Assert.equals("channel@example.com/server-id", channel.self.id); + Assert.equals("server-nick", channel.self.displayName); + Assert.isTrue(channel.isOwner()); + Assert.isTrue(channel.self.presence.iterator().hasNext()); + } + + public function testDisconnectedChannelRetainsRolePermissions(async: Async) { + final persistence = new ChannelMembersPersistence(); + final client = new Client("test@example.com", persistence); + final channel = new Channel(client, client.stream, persistence, "channel@example.com"); + channel.disco = new borogove.Caps("", [], ["http://jabber.org/protocol/muc", "muc_membersonly", "urn:xmpp:message-moderate:1"], []); + final owner = new Role("owner", "Owner"); + final self = new Member( + "me", "myself", null, true, [owner], JID.parse("test@example.com"), + ["myself" => new borogove.Presence(null, new Stanza("x", { xmlns: "http://jabber.org/protocol/muc#user" }).tag("item", { role: "moderator" }).up(), null)], + null + ); + channel.joined(self); + Assert.isTrue(channel.canSetSubject()); + Assert.isTrue(channel.canModerate()); + Assert.isTrue(channel.canInvite()); + Assert.isTrue(channel.isOwner()); + Assert.isTrue(channel.availableRoles(self).length > 0); + Assert.isTrue(channel.canRemoveRole(self, owner)); + + channel.disconnected(); + Assert.equals(self, channel.self); + Assert.isFalse(channel.canSetSubject()); + Assert.isFalse(channel.canModerate()); + Assert.isTrue(channel.canInvite()); + Assert.isTrue(channel.isOwner()); + Assert.isTrue(channel.availableRoles(self).length > 0); + Assert.isTrue(channel.canRemoveRole(self, owner)); + channel.members().then(_ -> { + Assert.isTrue(persistence.forModerator); + async.done(); + }); + } + + public function testDisconnectedVisitorCannotQueueMessages() { + final persistence = new Dummy(); + final client = new Client("test@example.com", persistence); + final channel = new Channel(client, client.stream, persistence, "channel@example.com"); + channel.joined(new Member( + "me", "myself", null, true, [], JID.parse("test@example.com"), + ["myself" => new borogove.Presence(null, new Stanza("x", { xmlns: "http://jabber.org/protocol/muc#user" }).tag("item", { role: "visitor" }).up(), null)], + null + )); + Assert.isFalse(channel.canSend()); + channel.disconnected(); + Assert.isFalse(channel.canSend()); + } + public function testJoinFailure() { final persistence = new Dummy(); final client = new Client("test@example.com", persistence); @@ -565,6 +669,7 @@ class TestChat extends utest.Test { Assert.equals(channel.joinFailed, errorStanza, "joinFailed should be set"); Assert.isFalse(channel.syncing(), "Should NOT be syncing after join failure"); + Assert.isFalse(channel.isJoined); } public function testJoinSuccess() { @@ -574,7 +679,8 @@ class TestChat extends utest.Test { final availableChat = new AvailableChat("channel@example.com", "Channel", "", caps); final channel = cast(client.startChat(availableChat), Channel); - Assert.isNull(channel.self); + Assert.notNull(channel.self); + Assert.isFalse(channel.isJoined); final joinedStanza = new Stanza("presence", { xmlns: "jabber:client", from: "channel@example.com/test", to: "test@example.com" }) .tag("x", { xmlns: "http://jabber.org/protocol/muc#user" }).tag("status", { code: "110" }).up() @@ -583,6 +689,55 @@ class TestChat extends utest.Test { client.stream.onStanza(joinedStanza); Assert.notNull(channel.self); + Assert.isTrue(channel.isJoined); + final self = channel.self; + + final unavailableStanza = new Stanza("presence", { xmlns: "jabber:client", from: "channel@example.com/test", to: "test@example.com", type: "unavailable" }) + .tag("x", { xmlns: "http://jabber.org/protocol/muc#user" }).tag("status", { code: "110" }).up() + .up(); + + client.stream.onStanza(unavailableStanza); + + Assert.isFalse(channel.isJoined); + Assert.equals(self, channel.self, "Unavailable presence should retain self"); + Assert.isFalse(channel.livePresence()); + } + + public function testRejoinRetainsSelf(async: Async) { + final persistence = new Dummy(); + final client = new Client("test@example.com", persistence); + final channel = new Channel(client, client.stream, persistence, "channel@example.com"); + final joinedStanza = new Stanza("presence", { xmlns: "jabber:client", from: "channel@example.com/test", to: "test@example.com" }) + .tag("x", { xmlns: "http://jabber.org/protocol/muc#user" }).tag("status", { code: "110" }).up() + .up(); + channel.setPresence("test", cast joinedStanza, false); + final self = channel.self; + Assert.isTrue(channel.isJoined); + + channel.join().then(_ -> { + Assert.isFalse(channel.isJoined); + Assert.equals(self, channel.self, "Rejoining should retain self until new presence arrives"); + + channel.setPresence("test", cast joinedStanza, false); + Assert.isTrue(channel.isJoined); + Assert.notNull(channel.self); + Assert.isFalse(self == channel.self, "New self presence should replace the cached member"); + async.done(); + }); + } + + public function testRestoredSelfPresenceJoinsChannel() { + final persistence = new Dummy(); + final client = new Client("test@example.com", persistence); + final channel = new Channel(client, client.stream, persistence, "channel@example.com"); + final joinedStanza = new Stanza("presence", { xmlns: "jabber:client", from: "channel@example.com/test", to: "test@example.com" }) + .tag("x", { xmlns: "http://jabber.org/protocol/muc#user" }).tag("status", { code: "110" }).up() + .up(); + + Assert.isFalse(channel.isJoined); + channel.setPresence("test", cast joinedStanza, true); + Assert.isTrue(channel.isJoined); + Assert.notNull(channel.self); } public function testSyncFailure(async: Async) { @@ -641,7 +796,7 @@ class TestChat extends utest.Test { final persistence = new Dummy(); final client = new Client("test@example.com", persistence); final chat = new Channel(client, client.stream, persistence, "channel@example.com"); - chat.self = new Member("me", "myself", null, true, [new Role("owner", "")], JID.parse("test@example.com"), new Map(), null); + chat.joined(new Member("me", "myself", null, true, [new Role("owner", "")], JID.parse("test@example.com"), new Map(), null)); final roles = chat.availableRoles(new Member("other", "other", null, true, [], JID.parse("test@example.com"), new Map(), null)); final ids = roles.map(r -> r.id); @@ -655,7 +810,7 @@ class TestChat extends utest.Test { final persistence = new Dummy(); final client = new Client("test@example.com", persistence); final chat = new Channel(client, client.stream, persistence, "channel@example.com"); - chat.self = new Member("me", "myself", null, true, [new Role("owner", "")], JID.parse("test@example.com"), new Map(), null); + chat.joined(new Member("me", "myself", null, true, [new Role("owner", "")], JID.parse("test@example.com"), new Map(), null)); final roles = chat.availableRoles(new Member("other", "other", null, true, [new Role("none", "")], JID.parse("test@example.com"), new Map(), null)); final ids = roles.map(r -> r.id); @@ -739,7 +894,7 @@ class TestChat extends utest.Test { final persistence = new ChannelMembersPersistence(); final client = new Client("test@example.com", persistence); final chat = new Channel(client, client.stream, persistence, "channel@example.com"); - chat.self = new Member("me", "myself", null, true, [new Role("admin", "Admin")], JID.parse("test@example.com"), new Map(), null); + chat.joined(new Member("me", "myself", null, true, [new Role("admin", "Admin")], JID.parse("test@example.com"), new Map(), null)); chat.members().then(members -> { Assert.isTrue(persistence.forModerator); @@ -772,10 +927,10 @@ class TestChat extends utest.Test { Assert.isFalse(chat.canSetPhoto()); - chat.self = new Member("me", "myself", null, true, [new Role("member", "")], JID.parse("test@example.com"), new Map(), null); + chat.joined(new Member("me", "myself", null, true, [new Role("member", "")], JID.parse("test@example.com"), new Map(), null)); Assert.isFalse(chat.canSetPhoto()); - chat.self = new Member("me", "myself", null, true, [new Role("owner", "")], JID.parse("test@example.com"), new Map(), null); + chat.joined(new Member("me", "myself", null, true, [new Role("owner", "")], JID.parse("test@example.com"), new Map(), null)); Assert.isTrue(chat.canSetPhoto()); } diff --git a/test/TestClient.hx b/test/TestClient.hx index 8b0fa6e..457711d 100644 --- a/test/TestClient.hx +++ b/test/TestClient.hx @@ -1116,7 +1116,9 @@ class TestClient extends utest.Test { }); client.startChatWith("room@example.com", _->Open, _->{ - Assert.equals(1, client.chats.length); + final rooms = client.chats.filter(c -> c.chatId == "room@example.com"); + Assert.equals(1, rooms.length); + Assert.equals(chat, rooms[0]); async.done(); }); } diff --git a/test/TestSqlite.hx b/test/TestSqlite.hx index 7e76a97..9052420 100644 --- a/test/TestSqlite.hx +++ b/test/TestSqlite.hx @@ -18,6 +18,7 @@ import borogove.JID; import borogove.ID; import borogove.Message; import borogove.Chat; +import borogove.Client; import borogove.Chat.AvailableChat; import borogove.Status; import borogove.Reaction; @@ -1245,7 +1246,8 @@ class TestSqlite extends utest.Test { public function testGetChatsHydratesMembersForNameAndMavUntil(async: Async) { final account = "alice@example.com"; - final chat = new Channel(cast null, cast null, persistence, "room-chat-hydrate@example.com"); + final client = new Client(account, persistence); + final chat = new Channel(client, client.stream, persistence, "room-chat-hydrate@example.com"); chat.displayName = "Tea Room"; chat.mavUntil = "2024-05-01T12:00:00Z"; @@ -1304,7 +1306,7 @@ class TestSqlite extends utest.Test { ]).then(_ -> { haxe.Timer.delay(() -> { persistence.getChats(account).then(chats -> { - final stored = chats[0]; + final stored = chats.filter(stored -> stored.chatId == chat.chatId)[0]; Assert.notNull(stored.membersForName); Assert.equals("2024-05-01T12:00:00Z", stored.mavUntil); Assert.equals(2, stored.membersForName.length); @@ -1340,7 +1342,8 @@ class TestSqlite extends utest.Test { public function testGetMembersHydratesPersistedMemberData(async: Async) { final account = "alice@example.com"; - final chat = new Channel(cast null, cast null, persistence, "room-members-1@example.com"); + final client = new Client(account, persistence); + final chat = new Channel(client, client.stream, persistence, "room-members-1@example.com"); chat.displayName = "A Chat"; chat.trusted = true; final member = new Member( @@ -1373,7 +1376,8 @@ class TestSqlite extends utest.Test { public function testStoreMemberUpdatesMergesExistingMemberData(async: Async) { final account = "alice@example.com"; - final chat = new Channel(cast null, cast null, persistence, "room-members-2@example.com"); + final client = new Client(account, persistence); + final chat = new Channel(client, client.stream, persistence, "room-members-2@example.com"); chat.displayName = "A Chat"; persistence.storeMembers(account, chat.chatId, [ @@ -1414,7 +1418,8 @@ class TestSqlite extends utest.Test { public function testStoreMemberUpdatesClearsOmittedFullListAffiliations(async: Async) { final account = "alice@example.com"; - final chat = new Channel(cast null, cast null, persistence, "room-members-2b@example.com"); + final client = new Client(account, persistence); + final chat = new Channel(client, client.stream, persistence, "room-members-2b@example.com"); chat.displayName = "A Chat"; persistence.storeMembers(account, chat.chatId, [ @@ -1470,7 +1475,8 @@ class TestSqlite extends utest.Test { public function testStoreMemberUpdatesMatchesExistingMemberByTrueJid(async: Async) { final account = "alice@example.com"; - final chat = new Channel(cast null, cast null, persistence, "room-members-3@example.com"); + final client = new Client(account, persistence); + final chat = new Channel(client, client.stream, persistence, "room-members-3@example.com"); chat.displayName = "A Chat"; persistence.storeMembers(account, chat.chatId, [ @@ -1509,9 +1515,10 @@ class TestSqlite extends utest.Test { public function testClearMemberPresenceOnlyClearsTargetedChat(async: Async) { final account = "alice@example.com"; - final chat1 = new Channel(cast null, cast null, persistence, "room-members-4a@example.com"); + final client = new Client(account, persistence); + final chat1 = new Channel(client, client.stream, persistence, "room-members-4a@example.com"); chat1.displayName = "A Chat"; - final chat2 = new Channel(cast null, cast null, persistence, "room-members-4b@example.com"); + final chat2 = new Channel(client, client.stream, persistence, "room-members-4b@example.com"); chat2.displayName = "A Chat"; persistence.storeMembers(account, chat1.chatId, [ @@ -1560,7 +1567,8 @@ class TestSqlite extends utest.Test { public function testGetMembersFiltersHiddenRowsForNonModerators(async: Async) { final account = "alice@example.com"; - final chat = new Channel(cast null, cast null, persistence, "room-members-5@example.com"); + final client = new Client(account, persistence); + final chat = new Channel(client, client.stream, persistence, "room-members-5@example.com"); chat.displayName = "A Chat"; persistence.storeMembers(account, chat.chatId, [ @@ -1581,7 +1589,8 @@ class TestSqlite extends utest.Test { public function testGetMembersTreatsEmptyPresenceAsOffline(async: Async) { final account = "alice@example.com"; - final chat = new Channel(cast null, cast null, persistence, "room-members-empty-presence@example.com"); + final client = new Client(account, persistence); + final chat = new Channel(client, client.stream, persistence, "room-members-empty-presence@example.com"); chat.displayName = "A Chat"; persistence.storeMembers(account, chat.chatId, [ @@ -1608,7 +1617,8 @@ class TestSqlite extends utest.Test { public function testGetMembersIncludesModeratorVisibleRows(async: Async) { final account = "alice@example.com"; - final chat = new Channel(cast null, cast null, persistence, "room-members-6@example.com"); + final client = new Client(account, persistence); + final chat = new Channel(client, client.stream, persistence, "room-members-6@example.com"); chat.displayName = "A Chat"; persistence.storeMembers(account, chat.chatId, [ @@ -1629,7 +1639,8 @@ class TestSqlite extends utest.Test { public function testGetMemberDetailsReturnsNullForIncompleteRows(async: Async) { final account = "alice@example.com"; - final chat = new Channel(cast null, cast null, persistence, "room-members-7@example.com"); + final client = new Client(account, persistence); + final chat = new Channel(client, client.stream, persistence, "room-members-7@example.com"); chat.displayName = "A Chat"; persistence.storeMembers(account, chat.chatId, [ @@ -1673,7 +1684,8 @@ class TestSqlite extends utest.Test { public function testVoiceRequests(async: Async) { final account = "alice@example.com"; - final chat = new Channel(cast null, cast null, persistence, "room-voice-requests@example.com"); + final client = new Client(account, persistence); + final chat = new Channel(client, client.stream, persistence, "room-voice-requests@example.com"); chat.displayName = "A Chat"; persistence.storeMembers(account, chat.chatId, [ diff --git a/test/idb.spec.ts b/test/idb.spec.ts index 98e71f5..7979960 100644 --- a/test/idb.spec.ts +++ b/test/idb.spec.ts @@ -9,8 +9,10 @@ async function idbFixture() { `${databaseName}-media`, ); const persistence = await borogove.persistence.IDB(databaseName, mediaStore); - const createChannel = (p: any, chatId: string) => - new borogove.Channel(null, null, p, chatId); + const createChannel = (p: any, chatId: string) => { + const client = new borogove.Client("alice@example.com", p); + return new borogove.Channel(client, client.stream, p, chatId); + }; const storeIncompleteMember = async (chatId: string) => { const request = indexedDB.open(databaseName); const db = await new Promise<IDBDatabase>((resolve, reject) => { diff --git a/test/persistence-tests.ts b/test/persistence-tests.ts index 0d1a01b..0aa77b1 100644 --- a/test/persistence-tests.ts +++ b/test/persistence-tests.ts @@ -230,7 +230,9 @@ export function sharedPersistenceTests(test) { chat: { chatId: "hidden@example.com" }, }, ]); - const [stored] = await persistence.getChats("alice@example.com"); + const stored = (await persistence.getChats("alice@example.com")).find( + (storedChat) => storedChat.chatId === chat.chatId, + ); expect(stored.mavUntil).toBe("2024-05-01T12:00:00Z"); expect(stored.membersForName.map((m) => m.displayName)).toEqual([ "Alpha", @@ -1624,13 +1626,9 @@ export function sharedPersistenceTests(test) { test("getMembers filters hidden rows for non-moderators", async ({ borogove, persistence, + createChannel, }) => { - const chat = new borogove.Channel( - null, - null, - persistence, - "room-members-5@example.com", - ); + const chat = createChannel(persistence, "room-members-5@example.com"); chat.displayName = "Tea Room"; chat.trusted = true; diff --git a/test/sqlite.spec.ts b/test/sqlite.spec.ts index 3efcccc..84885f6 100644 --- a/test/sqlite.spec.ts +++ b/test/sqlite.spec.ts @@ -30,8 +30,10 @@ async function sqliteFixture() { storeChats(...args); return new Promise((resolve) => setTimeout(resolve, 200)); }; - const createChannel = (p: any, chatId: string) => - new sqlite.Channel(null, null, p, chatId); + const createChannel = (p: any, chatId: string) => { + const client = new borogove.Client("alice@example.com", p); + return new sqlite.Channel(client, client.stream, p, chatId); + }; const storeIncompleteMember = async (chatId: string) => { await persistence.db.exec( "INSERT INTO members(account_id, chat_id, member_id, display_name, photo_uri, is_self, chat, roles, presence, jid) VALUES(?, ?, ?, ?, ?, ?, ?, jsonb(?), jsonb(?), ?)",