From 983fc1aab272a7eb83aee52fd4d6a3e6b5fb93b0 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Thu, 6 Aug 2026 10:41:12 +0300 Subject: [PATCH] refactor: move per-user metadata finder to commons on the new upstream model (Phase 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move UserFinderFilterAssembler(+QueryState), the four sub-assemblers (UserOutboxFinder, UserWatcher, UserReports, UserCards), FilterUserMetadataForKey, and FilterReportsToKey from amethyst/reqCommand/user/ into commons/relayClient/user/, plus the inner Account-free pickRelaysToLoadUsers overload into commons PickRelaysToLoadUsers.kt. Reconciled with upstream's re-architecture: - UserFinderQueryState carries the narrow UserFinderAccount (Phase 1) instead of the full Account; DROPS AccountScopedQuery. The finder sub-assemblers extend the commons base managers and attribute inline via soleAccountPubKey — now sourced from UserFinderAccount.userFinderPubkeyHex — so upstream's per-account attribution + ExplainedFilter/SubPurpose tags are preserved unchanged. - cache: LocalCache -> ICacheProvider; FilterUserMetadataForKey + pickRelaysToLoadUsers take an injected relayHints: HintIndexer instead of the static LocalCache.relayHints. - UserOutboxFinderSubAssembler inlines the relay-tier union over the UserFinderAccount getters (behaviour-preserving vs the old Account-based outer overload). Android unchanged: UserFinderShims.kt typealiases keep call sites (incl. reqCommand/event EventFinder*) compiling; UserFinderFilterAssemblerSubscription + UserObservers stay in amethyst (Account is-a UserFinderAccount). New commons UserFinderSubscription (LocalUserFinder/LocalUserFinderAccount + overloads) and UserMetadataObservers (observeUser*) added for Desktop. amethyst FilterFindFollowMetadataForKey's outer overload now delegates to the commons inner one. Green: commons JVM + iOS purity (verifyKmpPurity), :amethyst compilePlayDebugKotlin, :desktopApp compile, spotless. Co-Authored-By: Claude Opus 4.8 --- .../follows/FilterFindFollowMetadataForKey.kt | 121 +----------- .../reqCommand/user/UserFinderShims.kt | 30 +++ .../relayClient/user/PickRelaysToLoadUsers.kt | 147 +++++++++++++++ .../user/UserFinderFilterAssembler.kt | 22 +-- .../user/UserFinderSubscription.kt | 85 +++++++++ .../relayClient/user/UserMetadataObservers.kt | 177 ++++++++++++++++++ .../loaders/UserOutboxFinderSubAssembler.kt | 42 +++-- .../user/watchers/FilterReportsToKey.kt | 2 +- .../user/watchers/FilterUserMetadataForKey.kt | 11 +- .../user/watchers/UserCardsSubAssembler.kt | 24 +-- .../user/watchers/UserReportsSubAssembler.kt | 23 +-- .../user/watchers/UserWatcherSubAssembler.kt | 21 +-- 12 files changed, 521 insertions(+), 184 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderShims.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/PickRelaysToLoadUsers.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient}/user/UserFinderFilterAssembler.kt (74%) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/UserFinderSubscription.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/UserMetadataObservers.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient}/user/loaders/UserOutboxFinderSubAssembler.kt (81%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient}/user/watchers/FilterReportsToKey.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient}/user/watchers/FilterUserMetadataForKey.kt (94%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient}/user/watchers/UserCardsSubAssembler.kt (88%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient}/user/watchers/UserReportsSubAssembler.kt (89%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient}/user/watchers/UserWatcherSubAssembler.kt (87%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt index 4d3ed060cd..f466cb4f17 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/FilterFindFollowMetadataForKey.kt @@ -23,13 +23,13 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follow import com.vitorpamplona.amethyst.commons.defaults.Constants import com.vitorpamplona.amethyst.commons.defaults.DefaultIndexerRelayList import com.vitorpamplona.amethyst.commons.defaults.DefaultSearchRelayList +import com.vitorpamplona.amethyst.commons.relayClient.user.pickRelaysToLoadUsers import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.utils.mapOfSet fun pickRelaysToLoadUsers( users: Set, @@ -68,6 +68,7 @@ fun pickRelaysToLoadUsers( return pickRelaysToLoadUsers( users, + LocalCache.relayHints, indexRelays - cannotConnectRelays, homeRelays - cannotConnectRelays, searchRelays - cannotConnectRelays, @@ -77,121 +78,3 @@ fun pickRelaysToLoadUsers( hasTried, ) } - -fun pickRelaysToLoadUsers( - users: Set, - indexRelays: Set, - homeRelays: Set, - searchRelays: Set, - connected: Set, - commonRelays: Set, - cannotConnectRelays: Set, - hasTried: EOSEAccountFast, -): Map> = - mapOfSet { - users.forEachIndexed { _, key -> - val tried = (hasTried.since(key)?.keys ?: emptySet()) + cannotConnectRelays - - val outbox = key.authorRelayList()?.writeRelaysNorm() - - if (!outbox.isNullOrEmpty()) { - // If there is a home, get from it. - - // if it tried all outbox relays, stop. - // the UserWatch will take over from here. - val leftToTry = (outbox - tried) - leftToTry.forEach { - add(it, key.pubkeyHex) - } - } else { - // if not, tries hints first. - val hints = key.allUsedRelays() + LocalCache.relayHints.hintsForKey(key.pubkeyHex) - - val leftToTryOnHints = hints - tried - - leftToTryOnHints.forEach { - add(it, key.pubkeyHex) - } - - // if there are only a few hints, broadens the search - if (leftToTryOnHints.size < 3) { - // This creates a pre-deterministic order of the array such that - // if this function is called twice, it returns the same arrays - // which gets ignored by the relay client if we send it twice - val indexRelaysLeftToTry = - (indexRelays - tried).sortedBy { relay -> - key.pubkeyHex.hashCode() xor relay.url.hashCode() - } - // This creates a pre-deterministic order of the array such that - // if this function is called twice, it returns the same arrays - // which gets ignored by the relay client if we send it twice - val homeRelaysLeftToTry = - (homeRelays - tried).sortedBy { relay -> - key.pubkeyHex.hashCode() xor relay.url.hashCode() - } - - // picks one at random to avoid overloading these relays - if (users.size > 300) { - if (indexRelaysLeftToTry.size >= 2) { - add(indexRelaysLeftToTry[0], key.pubkeyHex) - add(indexRelaysLeftToTry[1], key.pubkeyHex) - } else if (indexRelaysLeftToTry.size == 1) { - add(indexRelaysLeftToTry.first(), key.pubkeyHex) - } - - homeRelaysLeftToTry.forEach { - add(it, key.pubkeyHex) - } - } else { - indexRelaysLeftToTry.forEach { - add(it, key.pubkeyHex) - } - - homeRelaysLeftToTry.forEach { - add(it, key.pubkeyHex) - } - } - - if (indexRelaysLeftToTry.size < 2) { - val searchRelaysLeftToTry = searchRelays - tried - - searchRelaysLeftToTry.forEach { - add(it, key.pubkeyHex) - } - - val connectedRelaysLeftToTry = - (connected - tried) - .sortedBy { relay -> - key.pubkeyHex.hashCode() xor relay.url.hashCode() - }.take(100) - - // picks one at random to avoid overloading these relays - if (users.size > 300) { - connectedRelaysLeftToTry.take(20).forEach { - add(it, key.pubkeyHex) - } - } else { - connectedRelaysLeftToTry.forEach { - add(it, key.pubkeyHex) - } - } - - if (searchRelaysLeftToTry.size < 2) { - // This creates a pre-deterministic order of the array such that - // if this function is called twice, it returns the same arrays - // which gets ignored by the relay client if we send it twice - val allRelaysLeftToTry = - (commonRelays - tried) - .sortedBy { relay -> - key.pubkeyHex.hashCode() xor relay.url.hashCode() - }.take(100) - - allRelaysLeftToTry.forEach { - add(it, key.pubkeyHex) - } - } - } - } - } - } - } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderShims.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderShims.kt new file mode 100644 index 0000000000..c9d01b3b68 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderShims.kt @@ -0,0 +1,30 @@ +/* + * 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.user + +/** + * Back-compat aliases: the per-user metadata finder moved to commons + * (`com.vitorpamplona.amethyst.commons.relayClient.user`). Existing Android call + * sites that reference these by their old names resolve here. + */ +typealias UserFinderFilterAssembler = com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderFilterAssembler + +typealias UserFinderQueryState = com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderQueryState diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/PickRelaysToLoadUsers.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/PickRelaysToLoadUsers.kt new file mode 100644 index 0000000000..289a2c96bf --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/PickRelaysToLoadUsers.kt @@ -0,0 +1,147 @@ +/* + * 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.commons.relayClient.user + +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.relays.EOSEAccountFast +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.HintIndexer +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.utils.mapOfSet + +fun pickRelaysToLoadUsers( + users: Set, + relayHints: HintIndexer, + indexRelays: Set, + homeRelays: Set, + searchRelays: Set, + connected: Set, + commonRelays: Set, + cannotConnectRelays: Set, + hasTried: EOSEAccountFast, +): Map> = + mapOfSet { + users.forEachIndexed { _, key -> + val tried = (hasTried.since(key)?.keys ?: emptySet()) + cannotConnectRelays + + val outbox = key.authorRelayList()?.writeRelaysNorm() + + if (!outbox.isNullOrEmpty()) { + // If there is a home, get from it. + + // if it tried all outbox relays, stop. + // the UserWatch will take over from here. + val leftToTry = (outbox - tried) + leftToTry.forEach { + add(it, key.pubkeyHex) + } + } else { + // if not, tries hints first. + val hints = key.allUsedRelays() + relayHints.hintsForKey(key.pubkeyHex) + + val leftToTryOnHints = hints - tried + + leftToTryOnHints.forEach { + add(it, key.pubkeyHex) + } + + // if there are only a few hints, broadens the search + if (leftToTryOnHints.size < 3) { + // This creates a pre-deterministic order of the array such that + // if this function is called twice, it returns the same arrays + // which gets ignored by the relay client if we send it twice + val indexRelaysLeftToTry = + (indexRelays - tried).sortedBy { relay -> + key.pubkeyHex.hashCode() xor relay.url.hashCode() + } + // This creates a pre-deterministic order of the array such that + // if this function is called twice, it returns the same arrays + // which gets ignored by the relay client if we send it twice + val homeRelaysLeftToTry = + (homeRelays - tried).sortedBy { relay -> + key.pubkeyHex.hashCode() xor relay.url.hashCode() + } + + // picks one at random to avoid overloading these relays + if (users.size > 300) { + if (indexRelaysLeftToTry.size >= 2) { + add(indexRelaysLeftToTry[0], key.pubkeyHex) + add(indexRelaysLeftToTry[1], key.pubkeyHex) + } else if (indexRelaysLeftToTry.size == 1) { + add(indexRelaysLeftToTry.first(), key.pubkeyHex) + } + + homeRelaysLeftToTry.forEach { + add(it, key.pubkeyHex) + } + } else { + indexRelaysLeftToTry.forEach { + add(it, key.pubkeyHex) + } + + homeRelaysLeftToTry.forEach { + add(it, key.pubkeyHex) + } + } + + if (indexRelaysLeftToTry.size < 2) { + val searchRelaysLeftToTry = searchRelays - tried + + searchRelaysLeftToTry.forEach { + add(it, key.pubkeyHex) + } + + val connectedRelaysLeftToTry = + (connected - tried) + .sortedBy { relay -> + key.pubkeyHex.hashCode() xor relay.url.hashCode() + }.take(100) + + // picks one at random to avoid overloading these relays + if (users.size > 300) { + connectedRelaysLeftToTry.take(20).forEach { + add(it, key.pubkeyHex) + } + } else { + connectedRelaysLeftToTry.forEach { + add(it, key.pubkeyHex) + } + } + + if (searchRelaysLeftToTry.size < 2) { + // This creates a pre-deterministic order of the array such that + // if this function is called twice, it returns the same arrays + // which gets ignored by the relay client if we send it twice + val allRelaysLeftToTry = + (commonRelays - tried) + .sortedBy { relay -> + key.pubkeyHex.hashCode() xor relay.url.hashCode() + }.take(100) + + allRelaysLeftToTry.forEach { + add(it, key.pubkeyHex) + } + } + } + } + } + } + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/UserFinderFilterAssembler.kt similarity index 74% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/UserFinderFilterAssembler.kt index b0383beda5..9e92c5213a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/UserFinderFilterAssembler.kt @@ -18,18 +18,16 @@ * 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.user +package com.vitorpamplona.amethyst.commons.relayClient.user import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider 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.User -import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders.UserOutboxFinderSubAssembler -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers.UserCardsSubAssembler -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers.UserReportsSubAssembler -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers.UserWatcherSubAssembler +import com.vitorpamplona.amethyst.commons.relayClient.user.loaders.UserOutboxFinderSubAssembler +import com.vitorpamplona.amethyst.commons.relayClient.user.watchers.UserCardsSubAssembler +import com.vitorpamplona.amethyst.commons.relayClient.user.watchers.UserReportsSubAssembler +import com.vitorpamplona.amethyst.commons.relayClient.user.watchers.UserWatcherSubAssembler import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker @@ -37,13 +35,13 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineT @Stable class UserFinderQueryState( val user: User, - override val account: Account, -) : AccountScopedQuery + val account: UserFinderAccount, +) @Stable class UserFinderFilterAssembler( client: INostrClient, - cache: LocalCache, + cache: ICacheProvider, failureTracker: RelayOfflineTracker, ) : ComposeSubscriptionManager() { val group = diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/UserFinderSubscription.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/UserFinderSubscription.kt new file mode 100644 index 0000000000..51dfc57ea3 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/UserFinderSubscription.kt @@ -0,0 +1,85 @@ +/* + * 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.commons.relayClient.user + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription + +/** + * The shared per-user metadata data source for the current front end. A front + * end provides this once near its composition root (Android via AppModules, + * Desktop via its subscriptions coordinator). Reading it without a provider is + * a programming error — the `observeUser*` composables must never be reachable + * from a composition that has no relay client (e.g. the Android `:napplet` + * sandbox process). + */ +val LocalUserFinder = + staticCompositionLocalOf { + error("LocalUserFinder not provided") + } + +/** + * The current logged-in account, in the narrow [UserFinderAccount] view the + * finder needs to route REQs. Provided alongside [LocalUserFinder]. + */ +val LocalUserFinderAccount = + staticCompositionLocalOf { + error("LocalUserFinderAccount not provided") + } + +/** + * Subscribes to relay updates for [user]'s metadata (and relay list / reports / + * contact cards) for as long as this composable is in composition, coalesced + * with every other on-screen user into batched REQs by [dataSource]. + * + * Because a `LazyColumn` composes only the visible window (+ a small prefetch + * buffer), this naturally means "load metadata only for users currently on + * screen" — the [LifecycleAwareKeyDataSourceSubscription] unsubscribes ~30s + * after the row leaves composition or the app is backgrounded. + */ +@Composable +fun UserFinderFilterAssemblerSubscription( + user: User, + account: UserFinderAccount, + dataSource: UserFinderFilterAssembler, +) { + // Different screens get their own query-state instance even when tracking + // the same user; the assembler dedups to one REQ per pubkey. + val state = remember(user, account) { UserFinderQueryState(user, account) } + + LifecycleAwareKeyDataSourceSubscription(state, dataSource) +} + +/** + * Convenience overload that reads the front end's [LocalUserFinder] and + * [LocalUserFinderAccount] from the composition. + */ +@Composable +fun UserFinderFilterAssemblerSubscription(user: User) { + UserFinderFilterAssemblerSubscription( + user = user, + account = LocalUserFinderAccount.current, + dataSource = LocalUserFinder.current, + ) +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/UserMetadataObservers.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/UserMetadataObservers.kt new file mode 100644 index 0000000000..c1d20a41d9 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/UserMetadataObservers.kt @@ -0,0 +1,177 @@ +/* + * 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.commons.relayClient.user + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.nip01Core.UserInfo +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map + +/** + * Shared, platform-agnostic observers for a single user's metadata (kind 0). + * + * Each observer both (a) opens a composition-scoped relay subscription for the + * user via [UserFinderFilterAssemblerSubscription] — so metadata is fetched only + * while the user is on screen — and (b) collects the resulting cache flow so the + * UI recomposes when the metadata arrives. The `(user)` overloads read the + * front end's [LocalUserFinder] / [LocalUserFinderAccount]; the explicit-param + * overloads are for callers that already hold both (and for tests). + * + * These are metadata-only. Richer per-user observers that depend on account + * subsystems not yet in commons (contact-card petnames, follow counts, + * bookmarks, statuses) remain in the Android layer for now and layer on top of + * the same subscription. + */ +@Composable +fun observeUserInfo( + user: User, + userFinder: UserFinderFilterAssembler, + account: UserFinderAccount, +): State { + UserFinderFilterAssemblerSubscription(user, account, userFinder) + return user.metadata().flow.collectAsStateWithLifecycle() +} + +@Composable +fun observeUserInfo(user: User): State = observeUserInfo(user, LocalUserFinder.current, LocalUserFinderAccount.current) + +@Composable +fun observeUserPicture( + user: User, + userFinder: UserFinderFilterAssembler, + account: UserFinderAccount, +): State { + UserFinderFilterAssemblerSubscription(user, account, userFinder) + + val flow = + remember(user) { + user + .metadata() + .flow + .map { it?.info?.picture } + .distinctUntilChanged() + } + + return flow.collectAsStateWithLifecycle( + user + .metadataOrNull() + ?.flow + ?.value + ?.info + ?.picture, + ) +} + +@Composable +fun observeUserPicture(user: User): State = observeUserPicture(user, LocalUserFinder.current, LocalUserFinderAccount.current) + +@Composable +fun observeUserBanner( + user: User, + userFinder: UserFinderFilterAssembler, + account: UserFinderAccount, +): State { + UserFinderFilterAssemblerSubscription(user, account, userFinder) + + val flow = + remember(user) { + user + .metadata() + .flow + .map { it?.info?.banner } + .distinctUntilChanged() + } + + return flow.collectAsStateWithLifecycle( + user + .metadataOrNull() + ?.flow + ?.value + ?.info + ?.banner, + ) +} + +@Composable +fun observeUserBanner(user: User): State = observeUserBanner(user, LocalUserFinder.current, LocalUserFinderAccount.current) + +@Composable +fun observeUserAboutMe( + user: User, + userFinder: UserFinderFilterAssembler, + account: UserFinderAccount, +): State { + UserFinderFilterAssemblerSubscription(user, account, userFinder) + + val flow = + remember(user) { + user + .metadata() + .flow + .map { it?.info?.about ?: "" } + .distinctUntilChanged() + } + + return flow.collectAsStateWithLifecycle( + user + .metadataOrNull() + ?.flow + ?.value + ?.info + ?.about ?: "", + ) +} + +@Composable +fun observeUserAboutMe(user: User): State = observeUserAboutMe(user, LocalUserFinder.current, LocalUserFinderAccount.current) + +/** + * The user's best available display name from their own metadata (kind 0), + * falling back to a truncated pubkey. Metadata-only: it does NOT apply the + * viewing account's private contact-card petname (that stays in the Android + * layer, which wraps this). + */ +@Composable +fun observeUserName( + user: User, + userFinder: UserFinderFilterAssembler, + account: UserFinderAccount, +): State { + UserFinderFilterAssemblerSubscription(user, account, userFinder) + + val flow = + remember(user) { + user + .metadata() + .flow + .map { it?.info?.bestName() ?: user.toBestDisplayName() } + .distinctUntilChanged() + } + + return flow.collectAsStateWithLifecycle(user.toBestDisplayName()) +} + +@Composable +fun observeUserName(user: User): State = observeUserName(user, LocalUserFinder.current, LocalUserFinderAccount.current) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/loaders/UserOutboxFinderSubAssembler.kt similarity index 81% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/loaders/UserOutboxFinderSubAssembler.kt index 7743fb7c7c..0243f98f69 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserOutboxFinderSubAssembler.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/loaders/UserOutboxFinderSubAssembler.kt @@ -18,18 +18,18 @@ * 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.user.loaders +package com.vitorpamplona.amethyst.commons.relayClient.user.loaders import com.vitorpamplona.amethyst.commons.defaults.DefaultIndexerRelayList import com.vitorpamplona.amethyst.commons.defaults.DefaultSearchRelayList +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.BaseEoseManager import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows.pickRelaysToLoadUsers -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState -import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast +import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderQueryState +import com.vitorpamplona.amethyst.commons.relayClient.user.pickRelaysToLoadUsers +import com.vitorpamplona.amethyst.commons.relays.EOSEAccountFast import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient @@ -44,7 +44,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils class UserOutboxFinderSubAssembler( client: INostrClient, - val cache: LocalCache, + val cache: ICacheProvider, val failureTracker: RelayOfflineTracker, allKeys: () -> Set, ) : BaseEoseManager(client, allKeys) { @@ -108,19 +108,37 @@ class UserOutboxFinderSubAssembler( // and the users being resolved are whoever is on screen rather than anyone's follow list — so // with several accounts active there is no single honest owner for a given filter, and // splitting the sweep per account would re-issue the same lookups once per account. - // Deduped by pubkey, not by `Account`: that class uses identity equality, so two objects for - // the same logged-in user would look like two accounts and suppress attribution entirely. + // Deduped by pubkey, not by account identity: two objects for the same logged-in user would + // look like two accounts and suppress attribution entirely. val soleAccountPubKey = accounts - .mapTo(mutableSetOf()) { it.userProfile().pubkeyHex } + .mapTo(mutableSetOf()) { it.userFinderPubkeyHex } .singleOrNull() + // Union of every asking account's relay tiers. The UserFinderAccount getters already apply the + // platform default fallbacks (index/search), matching the prior outer pickRelaysToLoadUsers. + val cannotConnect = failureTracker.cannotConnectRelays + val indexRelays = mutableSetOf() + val homeRelays = mutableSetOf() + val searchRelays = mutableSetOf() + val commonRelays = mutableSetOf() + accounts.forEach { account -> + indexRelays.addAll(account.indexRelays()) + homeRelays.addAll(account.outboxHomeRelays()) + searchRelays.addAll(account.searchRelays()) + commonRelays.addAll(account.commonRelays()) + } + val perRelayKeysBoth = pickRelaysToLoadUsers( noOutboxList, - accounts, + cache.relayHints, + indexRelays - cannotConnect, + homeRelays - cannotConnect, + searchRelays - cannotConnect, connectedRelays, - failureTracker.cannotConnectRelays, + commonRelays - cannotConnect, + cannotConnect, hasTried, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterReportsToKey.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/FilterReportsToKey.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterReportsToKey.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/FilterReportsToKey.kt index a369d824e7..d8b069cfa3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterReportsToKey.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/FilterReportsToKey.kt @@ -18,7 +18,7 @@ * 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.user.watchers +package com.vitorpamplona.amethyst.commons.relayClient.user.watchers import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/FilterUserMetadataForKey.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/FilterUserMetadataForKey.kt index e52167f763..23227231ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/FilterUserMetadataForKey.kt @@ -18,16 +18,16 @@ * 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.user.watchers +package com.vitorpamplona.amethyst.commons.relayClient.user.watchers +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast +import com.vitorpamplona.amethyst.commons.relays.EOSEAccountFast import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.HintIndexer import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -60,6 +60,7 @@ val UserMetadataForKeyKinds = fun filterUserMetadataForKey( authors: Set, + relayHints: HintIndexer, indexRelays: Set, cannotConnectRelays: Set, since: EOSEAccountFast, @@ -72,7 +73,7 @@ fun filterUserMetadataForKey( val relays = when { outbox == null -> - key.allUsedRelays() + LocalCache.relayHints.hintsForKey(key.pubkeyHex) + indexRelays + key.allUsedRelays() + relayHints.hintsForKey(key.pubkeyHex) + indexRelays // Outbox is published but exhausted (every relay either EOSE'd // or is known-unreachable) and metadata is still missing — // widen to indexers so a misconfigured outbox doesn't strand diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/UserCardsSubAssembler.kt similarity index 88% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/UserCardsSubAssembler.kt index 0404db46cb..9c4ffbc31e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/UserCardsSubAssembler.kt @@ -18,16 +18,16 @@ * 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.user.watchers +package com.vitorpamplona.amethyst.commons.relayClient.user.watchers +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.amethyst.commons.model.toHexSet import com.vitorpamplona.amethyst.commons.relayClient.assemblers.filterContactCardsToTargetKeysFromTrustedAccountsInTheRelay import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.SingleSubEoseManager -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState -import com.vitorpamplona.amethyst.service.relays.MutableTime -import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderQueryState +import com.vitorpamplona.amethyst.commons.relays.MutableTime +import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter @@ -38,7 +38,7 @@ import com.vitorpamplona.quartz.utils.mapOfSet class UserCardsSubAssembler( client: INostrClient, - val cache: LocalCache, + val cache: ICacheProvider, allKeys: () -> Set, ) : SingleSubEoseManager(client, allKeys) { override fun newEose( @@ -74,22 +74,22 @@ class UserCardsSubAssembler( // accounts, so with several active none of them owns a given filter. val soleAccountPubKey = accounts - .mapTo(mutableSetOf()) { it.userProfile().pubkeyHex } + .mapTo(mutableSetOf()) { it.userFinderPubkeyHex } .singleOrNull() val trustedAccounts: Map> = mapOfSet { accounts.forEach { account -> - account.homeRelays.flow.value.forEach { - add(it, account.userProfile().pubkeyHex) + account.cardHomeRelays().forEach { + add(it, account.userFinderPubkeyHex) } } - accounts.map { it.trustProviderList.liveUserRankProvider.value }.forEach { provider -> + accounts.map { it.trustProvider() }.forEach { provider -> if (provider != null) { add(provider.relayUrl, provider.pubkey) } } - accounts.map { it.trustProviderList.liveUserFollowerCount.value }.forEach { provider -> + accounts.map { it.followerCountProvider() }.forEach { provider -> if (provider != null) { add(provider.relayUrl, provider.pubkey) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/UserReportsSubAssembler.kt similarity index 89% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/UserReportsSubAssembler.kt index 4371a4f822..5731d42b64 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/UserReportsSubAssembler.kt @@ -18,16 +18,16 @@ * 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.user.watchers +package com.vitorpamplona.amethyst.commons.relayClient.user.watchers +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.amethyst.commons.model.toHexSet import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.SingleSubEoseManager -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState -import com.vitorpamplona.amethyst.service.relays.MutableTime -import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderAccount +import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderQueryState +import com.vitorpamplona.amethyst.commons.relays.MutableTime +import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter @@ -35,7 +35,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl class UserReportsSubAssembler( client: INostrClient, - val cache: LocalCache, + val cache: ICacheProvider, allKeys: () -> Set, ) : SingleSubEoseManager(client, allKeys) { override fun newEose( @@ -72,12 +72,13 @@ class UserReportsSubAssembler( } private fun filtersFor( - account: Account, + account: UserFinderAccount, lastUsersOnFilter: Set, ): List { - val accountPubKey = account.userProfile().pubkeyHex + val accountPubKey = account.userFinderPubkeyHex - return account.declaredFollowsPerOutboxRelay.value + return account + .declaredFollowsByOutboxRelay() .flatMap { (relay, trustedUsersInThisRelay) -> // this relay + accounts are where we could find reports. // we might have already loaded them, so let's separate new targets that were checked before from the others diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/UserWatcherSubAssembler.kt similarity index 87% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/UserWatcherSubAssembler.kt index f5381a9be1..673b71a977 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/UserWatcherSubAssembler.kt @@ -18,14 +18,13 @@ * 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.user.watchers +package com.vitorpamplona.amethyst.commons.relayClient.user.watchers -import com.vitorpamplona.amethyst.commons.defaults.DefaultIndexerRelayList +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.BaseEoseManager -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState -import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast +import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderQueryState +import com.vitorpamplona.amethyst.commons.relays.EOSEAccountFast import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker @@ -37,7 +36,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils class UserWatcherSubAssembler( client: INostrClient, - val cache: LocalCache, + val cache: ICacheProvider, val failureTracker: RelayOfflineTracker, allKeys: () -> Set, ) : BaseEoseManager(client, allKeys) { @@ -100,20 +99,18 @@ class UserWatcherSubAssembler( // account and the users are whoever is on screen, so with several askers none of them owns it. val soleAccountPubKey = keys - .mapTo(mutableSetOf()) { it.account.userProfile().pubkeyHex } + .mapTo(mutableSetOf()) { it.account.userFinderPubkeyHex } .singleOrNull() val indexRelays = mutableSetOf() keys.mapTo(mutableSetOf()) { it.account }.forEach { - indexRelays.addAll( - it.indexerRelayList.flow.value - .ifEmpty { DefaultIndexerRelayList }, - ) + indexRelays.addAll(it.indexRelays()) } val newFilters = filterUserMetadataForKey( users, + cache.relayHints, indexRelays, failureTracker.cannotConnectRelays, latestEOSEs,