diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt index 227532d92d..29c77321af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt @@ -101,8 +101,8 @@ class RelaySubscriptionsCoordinator( // loaders of content that is not yet in the device. // they are active when looking at events, users, channels. val channelFinder = ChannelFinderFilterAssemblyGroup(client) - val eventFinder = EventFinderFilterAssembler(client) val userFinder = UserFinderFilterAssembler(client, cache, failureTracker) + val eventFinder = EventFinderFilterAssembler(client, cache, userFinder) // active when searching or tagging users. val search = SearchFilterAssembler(client, scope, cache) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt index 29a6767490..3e00e65548 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt @@ -23,9 +23,12 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.AddressableAuthorRelayLoaderSubAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.NoteEventLoaderSubAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers.EventWatcherSubAssembler +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssembler import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient // This allows multiple screen to be listening to tags, even the same tag @@ -38,11 +41,14 @@ class EventFinderQueryState( @Stable class EventFinderFilterAssembler( client: INostrClient, + cache: LocalCache, + userFinder: UserFinderFilterAssembler, ) : ComposeSubscriptionManager() { val group = listOf( NoteEventLoaderSubAssembler(client, ::allKeys), EventWatcherSubAssembler(client, ::allKeys), + AddressableAuthorRelayLoaderSubAssembler(cache, ::allKeys, userFinder), ) override fun invalidateFilters() = group.forEach { it.invalidateFilters() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/AddressableAuthorRelayLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/AddressableAuthorRelayLoaderSubAssembler.kt new file mode 100644 index 0000000000..5f81cb8d22 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/AddressableAuthorRelayLoaderSubAssembler.kt @@ -0,0 +1,74 @@ +/* + * 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.amethyst.service.relayClient.reqCommand.event.loaders + +import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.IEoseManager +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssembler +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState + +/** + * Bridges missing-addressable-note authors into [UserFinderFilterAssembler]. + * + * When an [AddressableNote] stub (event == null) references an author whose NIP-65 relay list + * has not been loaded yet, [potentialRelaysToFindAddress] returns an empty set and the event + * is never fetched. This manager detects those cases and injects a [UserFinderQueryState] for + * the author into the existing [UserFinderFilterAssembler], which already knows how to fetch + * kind-0 / kind-10002 and resolve outbox relays via [UserOutboxFinderSubAssembler]. Once the + * relay list arrives, [EventFinderFilterAssembler] is invalidated and can query the correct relay. + */ +class AddressableAuthorRelayLoaderSubAssembler( + val cache: LocalCache, + val allKeys: () -> Set, + val userFinder: UserFinderFilterAssembler, +) : IEoseManager { + private val activeSubscriptions = mutableSetOf() + + override fun invalidateFilters(ignoreIfDoing: Boolean) { + val needed = mutableSetOf() + + allKeys().forEach { key -> + val note = key.note + if (note is AddressableNote && note.event == null) { + val author = cache.getOrCreateUser(note.address.pubKeyHex) + if (author.authorRelayList() == null) { + needed.add(UserFinderQueryState(author, key.account)) + } + } + } + + val toAdd = needed - activeSubscriptions + val toRemove = activeSubscriptions - needed + + userFinder.subscribe(toAdd.toList()) + userFinder.unsubscribe(toRemove.toList()) + + activeSubscriptions.clear() + activeSubscriptions.addAll(needed) + } + + override fun destroy() { + userFinder.unsubscribe(activeSubscriptions.toList()) + activeSubscriptions.clear() + } +}