From 064a732ec80bc7735e1291b3f34b371267e69a7f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 23 Jul 2025 08:44:41 -0400 Subject: [PATCH] Fixes leaving and joining channels --- .../vitorpamplona/amethyst/model/Account.kt | 2 +- .../amethyst/model/LocalCache.kt | 33 ++++++----- .../model/emphChat/EphemeralChatListState.kt | 1 - .../experimental/ephemChat/chat/RoomId.kt | 2 +- .../ephemChat/list/EphemeralChatListEvent.kt | 12 +++- .../nip28PublicChat/list/ChannelListEvent.kt | 6 +- .../quartz/nip51Lists/TagArrayExt.kt | 13 ++++ .../quartz/nip51Lists/TagArrayExt.kt | 59 +++++++++++++++++++ 8 files changed, 105 insertions(+), 23 deletions(-) create mode 100644 quartz/src/test/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 32c66aab6..46367f35e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -854,8 +854,8 @@ class Account( fun sendMyPublicAndPrivateOutbox(event: Event?) { if (event == null) return - client.send(event, outboxRelays.flow.value) cache.justConsumeMyOwnEvent(event) + client.send(event, outboxRelays.flow.value) } fun sendMyPublicAndPrivateOutbox(events: List) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 7974342bf..1fab347ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -1571,14 +1571,16 @@ object LocalCache : ILocalCache { val new = consumeRegularEvent(event, relay, wasVerified) - val channel = checkGetOrCreatePublicChatChannel(channelId) - if (channel == null) { - Log.w("LocalCache", "Unable to create public chat channel for event ${event.toJson()}") - return false - } + if (new) { + val channel = checkGetOrCreatePublicChatChannel(channelId) + if (channel == null) { + Log.w("LocalCache", "Unable to create public chat channel for event ${event.toJson()}") + return false + } - val note = getOrCreateNote(event.id) - channel.addNote(note, relay) + val note = getOrCreateNote(event.id) + channel.addNote(note, relay) + } return new } @@ -1592,9 +1594,11 @@ object LocalCache : ILocalCache { val new = consumeRegularEvent(event, relay, wasVerified) - val note = getOrCreateNote(event.id) - val channel = getOrCreateEphemeralChannel(roomId) - channel.addNote(note, relay) + if (new) { + val note = getOrCreateNote(event.id) + val channel = getOrCreateEphemeralChannel(roomId) + channel.addNote(note, relay) + } return new } @@ -1608,10 +1612,11 @@ object LocalCache : ILocalCache { val new = consumeRegularEvent(event, relay, wasVerified) - val channel = getOrCreateLiveChannel(activityAddress) - - val note = getOrCreateNote(event.id) - channel.addNote(note, relay) + if (new) { + val channel = getOrCreateLiveChannel(activityAddress) + val note = getOrCreateNote(event.id) + channel.addNote(note, relay) + } return new } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt index b8a536821..0a9378ebb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt @@ -97,7 +97,6 @@ class EphemeralChatListState( suspend fun unfollow(channel: EphemeralChatChannel): EphemeralChatListEvent? { val ephemeralChatList = getEphemeralChatList() - return if (ephemeralChatList != null) { EphemeralChatListEvent.remove( earlierVersion = ephemeralChatList, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/RoomId.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/RoomId.kt index 42cc41a3c..f4ca23a74 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/RoomId.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/RoomId.kt @@ -41,5 +41,5 @@ data class RoomId( } } - fun toTagArray() = RoomIdTag.assemble(id, relayUrl) + fun toTagArray() = RoomIdTag.assemble(this) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/EphemeralChatListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/EphemeralChatListEvent.kt index 37bbaf1bb..9a9de6d9d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/EphemeralChatListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/EphemeralChatListEvent.kt @@ -23,6 +23,8 @@ package com.vitorpamplona.quartz.experimental.ephemChat.list import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.experimental.ephemChat.list.rooms +import com.vitorpamplona.quartz.experimental.ephemChat.list.tags.RoomIdTag +import com.vitorpamplona.quartz.experimental.ephemChat.list.tags.RoomIdTag.Companion.parse import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder @@ -35,7 +37,7 @@ import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent -import com.vitorpamplona.quartz.nip51Lists.remove +import com.vitorpamplona.quartz.nip51Lists.removeParsing import com.vitorpamplona.quartz.utils.TimeUtils import java.lang.reflect.Modifier.isPrivate @@ -48,6 +50,10 @@ class EphemeralChatListEvent( content: String, sig: HexKey, ) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun publicRooms() = tags.rooms() + + fun publicRoomSet() = tags.roomSet() + companion object { const val KIND = 10023 const val ALT = "Ephemeral Chat List" @@ -109,8 +115,8 @@ class EphemeralChatListEvent( ): EphemeralChatListEvent { val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() return resign( - privateTags = privateTags.remove(room.toTagArray()), - tags = earlierVersion.tags.remove(room.toTagArray()), + privateTags = privateTags.removeParsing(RoomIdTag::parse, room), + tags = earlierVersion.tags.removeParsing(RoomIdTag::parse, room), signer = signer, createdAt = createdAt, ) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt index 18f3f373e..49e72ed80 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt @@ -37,8 +37,8 @@ import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent import com.vitorpamplona.quartz.nip51Lists.encryption.signNip51List -import com.vitorpamplona.quartz.nip51Lists.remove import com.vitorpamplona.quartz.nip51Lists.removeAny +import com.vitorpamplona.quartz.nip51Lists.removeParsing import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -142,8 +142,8 @@ class ChannelListEvent( ): ChannelListEvent { val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() return resign( - privateTags = privateTags.remove(channel.toTagArray()), - tags = earlierVersion.tags.remove(channel.toTagArray()), + privateTags = privateTags.removeParsing(ChannelTag::parseId, channel.eventId), + tags = earlierVersion.tags.removeParsing(ChannelTag::parseId, channel.eventId), signer = signer, createdAt = createdAt, ) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt index e24563214..0b03a2b19 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt @@ -20,9 +20,11 @@ */ package com.vitorpamplona.quartz.nip51Lists +import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.utils.startsWith import com.vitorpamplona.quartz.utils.startsWithAny +import kotlin.collections.ArrayList inline fun TagArray.filterToArray(predicate: (Array) -> Boolean): TagArray = filterTo(ArrayList(), predicate).toTypedArray() @@ -30,6 +32,17 @@ inline fun TagArray.remove(predicate: (Array) -> Boolean): TagArray = fi fun TagArray.remove(startsWith: Array): TagArray = filterNotTo(ArrayList(this.size), { it.startsWith(startsWith) }).toTypedArray() +fun TagArray.removeParsing( + transform: (Tag) -> R, + equalsTo: R, +): TagArray = + filterNotTo( + destination = ArrayList(this.size), + predicate = { + transform(it) == equalsTo + }, + ).toTypedArray() + fun TagArray.removeAny(startsWith: List>): TagArray = filterNotTo( ArrayList(this.size), diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt b/quartz/src/test/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt new file mode 100644 index 000000000..b5c40011e --- /dev/null +++ b/quartz/src/test/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists + +import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId +import com.vitorpamplona.quartz.experimental.ephemChat.list.tags.RoomIdTag +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import junit.framework.TestCase.assertTrue +import org.junit.Test +import java.util.Arrays + +class TagArrayExt { + val tags = + arrayOf( + arrayOf("group", "test", "wss://nos.lol"), + arrayOf("group", "_", "wss://nos.lol"), + ) + + val expectedTags = + arrayOf( + arrayOf("group", "test", "wss://nos.lol"), + ) + + @Test + fun testRemove() { + assertTrue( + Arrays.deepEquals(expectedTags, tags.remove(arrayOf("group", "_", "wss://nos.lol"))), + ) + } + + @Test + fun testRemoveParsing() { + val removing = RoomId("_", RelayUrlNormalizer.normalize("wss://nos.lol")) + assertTrue( + Arrays.deepEquals( + expectedTags, + tags.removeParsing(RoomIdTag::parse, removing), + ), + ) + } +}