From a94c8673d5c3a4be72f1280b3adf2631ca39e656 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 10 Jul 2025 19:42:18 -0400 Subject: [PATCH] Fixing Ephemeral Chat interface --- .../vitorpamplona/amethyst/model/Account.kt | 34 +--- .../amethyst/model/LocalCache.kt | 155 ++++-------------- .../loggedIn/home/dal/HomeLiveFilter.kt | 4 +- .../experimental/ephemChat/chat/RoomId.kt | 11 +- .../ephemChat/chat/tags/RelayTag.kt | 2 +- .../client/single/basic/BasicRelayClient.kt | 1 - 6 files changed, 46 insertions(+), 161 deletions(-) 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 22c64adb5..a45ccfef2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -70,7 +70,6 @@ import com.vitorpamplona.amethyst.service.uploads.FileHeader import com.vitorpamplona.amethyst.ui.tor.TorType import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent -import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent @@ -145,9 +144,6 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay import com.vitorpamplona.quartz.nip19Bech32.entities.NSec -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent -import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag import com.vitorpamplona.quartz.nip30CustomEmoji.emojis import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent @@ -163,8 +159,6 @@ import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent -import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip56Reports.ReportType import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent @@ -815,33 +809,7 @@ class Account( ?: cache.relayHints.hintsForKey(pubkey).toSet() } - private fun computeRelaysForChannels(event: Event): Set { - val isInChannel = - if ( - event is ChannelMessageEvent || - event is ChannelMetadataEvent || - event is ChannelCreateEvent || - event is LiveActivitiesChatMessageEvent || - event is LiveActivitiesEvent || - event is EphemeralChatEvent - ) { - (event as? ChannelMessageEvent)?.channelId() - ?: (event as? ChannelMetadataEvent)?.channelId() - ?: (event as? ChannelCreateEvent)?.id - ?: (event as? LiveActivitiesChatMessageEvent)?.activity()?.toTag() - ?: (event as? LiveActivitiesEvent)?.aTag()?.toTag() - ?: (event as? EphemeralChatEvent)?.roomId()?.toKey() - } else { - null - } - - return if (isInChannel != null) { - val channel = LocalCache.checkGetOrCreateChannel(isInChannel) - channel?.relays() ?: emptySet() - } else { - emptySet() - } - } + private fun computeRelaysForChannels(event: Event): Set = LocalCache.getAnyChannel(event)?.relays() ?: emptySet() fun computeRelayListToBroadcast(event: Event): Set { if (event is MetadataEvent || event is AdvertisedRelayListEvent) { 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 4f13d4f56..a50c55a56 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -61,7 +61,6 @@ import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address @@ -416,33 +415,16 @@ object LocalCache : ILocalCache { EphemeralChatChannel(key) } - fun checkGetOrCreateChannel(key: String): Channel? { - checkNotInMainThread() - - if (key.contains("@")) { - val idParts = key.split("@") - val relay = RelayUrlNormalizer.normalizeOrNull(idParts[1]) - - if (relay == null) { - return null - } else { - getOrCreateEphemeralChannel(RoomId(idParts[0], relay)) - } - } - + fun checkGetOrCreatePublicChatChannel(key: String): PublicChatChannel? { if (isValidHex(key)) { return getOrCreatePublicChatChannel(key) } - - val address = Address.parse(key) - if (address != null) { - return getOrCreateLiveChannel(address) - } return null } private fun isValidHex(key: String): Boolean { if (key.isBlank()) return false + if (key.length != 64) return false if (key.contains(":")) return false return Hex.isHex(key) @@ -1622,12 +1604,12 @@ object LocalCache : ILocalCache { if (channelId.isNullOrBlank()) return false // new event - val oldChannel = checkGetOrCreateChannel(channelId) ?: return false + val oldChannel = checkGetOrCreatePublicChatChannel(channelId) ?: return false val author = getOrCreateUser(event.pubKey) val isVerified = if (event.createdAt > oldChannel.updatedMetadataAt) { - if (oldChannel is PublicChatChannel && (wasVerified || justVerify(event))) { + if (wasVerified || justVerify(event)) { oldChannel.updateChannelInfo(author, event) true } else { @@ -1653,44 +1635,18 @@ object LocalCache : ILocalCache { relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { - val channelId = event.channelId() + val channelId = event.channelId() ?: return false - if (channelId.isNullOrBlank()) return false - - val channel = checkGetOrCreateChannel(channelId) ?: return false + 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 author = getOrCreateUser(event.pubKey) - - if (relay != null) { - author.addRelayBeingUsed(relay, event.createdAt) - note.addRelay(relay) - } - - // Already processed this event. - if (note.event != null) return false - - if (antiSpam.isSpam(event, relay)) { - return false - } - - if (wasVerified || justVerify(event)) { - val replyTo = computeReplyTo(event) - - note.loadEvent(event, author, replyTo) - - // Log.d("CM", "New Chat Note (${note.author?.toBestDisplayName()} ${note.event?.content} - // ${formattedDateTime(event.createdAt)}") - - // Counts the replies - replyTo.forEach { it.addReply(note) } - - refreshObservers(note) - } - - return true + return consumeRegularEvent(event, relay, wasVerified) } fun consume( @@ -1698,46 +1654,15 @@ object LocalCache : ILocalCache { relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { - val roomId = event.roomId() - if (roomId == null) return false - - val channelId = roomId - val channel = getOrCreateEphemeralChannel(channelId) ?: return false + val roomId = event.roomId() ?: return false val note = getOrCreateNote(event.id) + val channel = getOrCreateEphemeralChannel(roomId) channel.addNote(note, relay) - val author = getOrCreateUser(event.pubKey) - - if (relay != null) { - author.addRelayBeingUsed(relay, event.createdAt) - note.addRelay(relay) - } - - // Already processed this event. - if (note.event != null) return false - - if (antiSpam.isSpam(event, relay)) { - return false - } - - if (wasVerified || justVerify(event)) { - note.loadEvent(event, author, emptyList()) - - refreshObservers(note) - - return true - } - - return false + return consumeRegularEvent(event, relay, wasVerified) } - fun consume( - event: CommentEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ) = consumeRegularEvent(event, relay, wasVerified) - fun consume( event: LiveActivitiesChatMessageEvent, relay: NormalizedRelayUrl?, @@ -1750,36 +1675,15 @@ object LocalCache : ILocalCache { val note = getOrCreateNote(event.id) channel.addNote(note, relay) - val author = getOrCreateUser(event.pubKey) - - if (relay != null) { - author.addRelayBeingUsed(relay, event.createdAt) - note.addRelay(relay) - } - - // Already processed this event. - if (note.event != null) return false - - if (antiSpam.isSpam(event, relay)) { - return false - } - - if (wasVerified || justVerify(event)) { - val replyTo = computeReplyTo(event) - - note.loadEvent(event, author, replyTo) - - // Counts the replies - replyTo.forEach { it.addReply(note) } - - refreshObservers(note) - - return true - } - - return false + return consumeRegularEvent(event, relay, wasVerified) } + fun consume( + event: CommentEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ) = consumeRegularEvent(event, relay, wasVerified) + @Suppress("UNUSED_PARAMETER") fun consume( event: ChannelHideMessageEvent, @@ -2859,18 +2763,18 @@ object LocalCache : ILocalCache { } } is EphemeralChatEvent -> { - draft.roomId()?.toKey()?.let { - checkGetOrCreateChannel(it)?.addNote(note, null) + draft.roomId()?.let { + getOrCreateEphemeralChannel(it).addNote(note, null) } } is ChannelMessageEvent -> { draft.channelId()?.let { channelId -> - checkGetOrCreateChannel(channelId)?.addNote(note, null) + checkGetOrCreatePublicChatChannel(channelId)?.addNote(note, null) } } is LiveActivitiesChatMessageEvent -> { draft.activityAddress()?.let { channelId -> - checkGetOrCreateChannel(channelId.toValue())?.addNote(note, null) + getOrCreateLiveChannel(channelId).addNote(note, null) } } is TextNoteEvent -> { @@ -2937,12 +2841,17 @@ object LocalCache : ILocalCache { } is ChannelMessageEvent -> { draft.channelId()?.let { channelId -> - checkGetOrCreateChannel(channelId)?.removeNote(draftWrap) + getPublicChatChannelIfExists(channelId)?.removeNote(draftWrap) } } is EphemeralChatEvent -> { draft.roomId()?.let { - getOrCreateEphemeralChannel(it).removeNote(draftWrap) + getEphemeralChatChannelIfExists(it)?.removeNote(draftWrap) + } + } + is LiveActivitiesChatMessageEvent -> { + draft.activityAddress()?.let { channelId -> + getLiveActivityChannelIfExists(channelId)?.removeNote(draftWrap) } } is TextNoteEvent -> { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt index ae1a0f7a7..7da242082 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt @@ -124,7 +124,7 @@ class HomeLiveFilter( } fun sort(collection: Set): List { - val topFilter = account.liveDiscoveryFollowLists.value + val topFilter = account.liveHomeFollowLists.value val topFilterAuthors = when (topFilter) { is AuthorsByOutboxTopNavFilter -> topFilter.authors @@ -137,7 +137,7 @@ class HomeLiveFilter( val followingKeySet = topFilterAuthors ?: account.kind3FollowList.flow.value.authors val followCounts = - collection.associate { it to followsThatParticipateOn(it, followingKeySet) } + collection.associateWith { followsThatParticipateOn(it, followingKeySet) } return collection.sortedWith( compareByDescending { followCounts[it] } 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 d249be351..884b19def 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 @@ -26,8 +26,17 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl data class RoomId( val id: String, val relayUrl: NormalizedRelayUrl, -) { +) : Comparable { fun toKey() = "$id@$relayUrl" fun toDisplayKey() = id + "@" + relayUrl.displayUrl() + + override fun compareTo(other: RoomId): Int { + val result = id.compareTo(other.id) + return if (result == 0) { + relayUrl.url.compareTo(other.relayUrl.url) + } else { + result + } + } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/tags/RelayTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/tags/RelayTag.kt index 4f596e31e..3399d98fc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/tags/RelayTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/tags/RelayTag.kt @@ -39,7 +39,7 @@ class RelayTag { ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].isNotEmpty()) { return null } - return RelayUrlNormalizer.normalizeOrNull(tag[1]) ?: return null + return RelayUrlNormalizer.normalizeOrNull(tag[1]) } @JvmStatic diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt index fcfac8e72..b4faad56b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt @@ -149,7 +149,6 @@ open class BasicRelayClient( override fun onMessage(text: String) { // Log.d(logTag, "Receiving: $text") - stats.addBytesReceived(text.bytesUsedInMemory()) try {