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 ec3df21db..c6562a000 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -2357,7 +2357,7 @@ class Account( suspend fun requestDVMContentDiscovery( dvmPublicKey: User, - onReady: (event: NIP90ContentDiscoveryRequestEvent) -> Unit, + onReady: (event: NIP90ContentDiscoveryRequestEvent, relays: Set) -> Unit, ) { val relays = nip65RelayList.inboxFlow.value.toSet() val request = signer.sign(NIP90ContentDiscoveryRequestEvent.build(dvmPublicKey.pubkeyHex, signer.pubKey, relays)) @@ -2367,7 +2367,7 @@ class Account( ?: (dvmPublicKey.allUsedRelays() + cache.relayHints.hintsForKey(dvmPublicKey.pubkeyHex)) cache.justConsumeMyOwnEvent(request) - onReady(request) + onReady(request, relayList.toSet()) delay(100) client.publish(request, relayList) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/dvms/FavoriteDvmOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/dvms/FavoriteDvmOrchestrator.kt index f4058cccb..cd92afa0f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/dvms/FavoriteDvmOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/dvms/FavoriteDvmOrchestrator.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip90Dvms.status.NIP90StatusEvent import com.vitorpamplona.quartz.utils.Log @@ -44,12 +45,16 @@ import kotlinx.coroutines.sync.withLock * Immutable snapshot of a favourite DVM's current request/response state. * * - [requestId] is the id of the most recently published kind-5300 request. + * - [responseRelays] is the relay set the kind-5300 was sent to — the same set on + * which the DVM will publish its 6300/7000 responses, so the home subscription + * manager must listen there (not on the user's own outbox). * - [ids] and [addresses] are the note references returned by the latest kind-6300 response. * - [latestStatus] is the latest kind-7000 status event (processing, payment-required, error, …). * - [errorMessage] captures any client-side failure while publishing the request. */ data class FavoriteDvmSnapshot( val requestId: HexKey? = null, + val responseRelays: Set = emptySet(), val ids: Set = emptySet(), val addresses: Set = emptySet(), val latestStatus: NIP90StatusEvent? = null, @@ -114,10 +119,11 @@ class FavoriteDvmOrchestrator( val job = scope.launch(Dispatchers.IO) { try { - account.requestDVMContentDiscovery(user) { request -> + account.requestDVMContentDiscovery(user) { request, relays -> seed.update { it.copy( requestId = request.id, + responseRelays = relays, ids = emptySet(), addresses = emptySet(), latestStatus = null, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteDvm/FavoriteDvmFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteDvm/FavoriteDvmFeedFlow.kt index f919d62a0..04d35e0d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteDvm/FavoriteDvmFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteDvm/FavoriteDvmFeedFlow.kt @@ -43,12 +43,13 @@ class FavoriteDvmFeedFlow( private fun buildFilter( snapshot: com.vitorpamplona.amethyst.model.dvms.FavoriteDvmSnapshot, - relays: Set, + contentRelays: Set, ) = FavoriteDvmTopNavFilter( dvmAddress = dvmAddress, acceptedIds = snapshot.ids, acceptedAddresses = snapshot.addresses, - relayList = relays, + contentRelays = contentRelays, + listenRelays = snapshot.responseRelays, requestId = snapshot.requestId, ) @@ -60,7 +61,7 @@ class FavoriteDvmFeedFlow( override fun startValue(): FavoriteDvmTopNavFilter = buildFilter( snapshot = orchestrator.observe(dvmAddress).value, - relays = resolveRelays(outboxRelays.value, proxyRelays.value), + contentRelays = resolveRelays(outboxRelays.value, proxyRelays.value), ) override suspend fun startValue(collector: FlowCollector) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteDvm/FavoriteDvmTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteDvm/FavoriteDvmTopNavFilter.kt index 093fc390d..e521c6d5b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteDvm/FavoriteDvmTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteDvm/FavoriteDvmTopNavFilter.kt @@ -43,7 +43,8 @@ class FavoriteDvmTopNavFilter( val dvmAddress: Address, val acceptedIds: Set, val acceptedAddresses: Set, - val relayList: Set, + val contentRelays: Set, + val listenRelays: Set, val requestId: HexKey?, ) : IFeedTopNavFilter { override fun matchAuthor(pubkey: HexKey): Boolean = true @@ -56,13 +57,14 @@ class FavoriteDvmTopNavFilter( override fun startValue(cache: LocalCache): FavoriteDvmTopNavPerRelayFilterSet = FavoriteDvmTopNavPerRelayFilterSet( - relayList.associateWith { - FavoriteDvmTopNavPerRelayFilter( - dvmPubkey = dvmAddress.pubKeyHex, - requestId = requestId, - ids = acceptedIds, - addresses = acceptedAddresses, - ) - }, + contentFetches = + contentRelays.associateWith { + FavoriteDvmTopNavPerRelayFilter( + ids = acceptedIds, + addresses = acceptedAddresses, + ) + }, + listenRelays = listenRelays, + requestId = requestId, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteDvm/FavoriteDvmTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteDvm/FavoriteDvmTopNavPerRelayFilter.kt index 5a8f3d7f6..4dcbeddaa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteDvm/FavoriteDvmTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteDvm/FavoriteDvmTopNavPerRelayFilter.kt @@ -26,8 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey @Immutable class FavoriteDvmTopNavPerRelayFilter( - val dvmPubkey: HexKey, - val requestId: HexKey?, val ids: Set, val addresses: Set, ) : IFeedTopNavPerRelayFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteDvm/FavoriteDvmTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteDvm/FavoriteDvmTopNavPerRelayFilterSet.kt index 4ec47da11..321f5ca2c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteDvm/FavoriteDvmTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteDvm/FavoriteDvmTopNavPerRelayFilterSet.kt @@ -21,8 +21,19 @@ package com.vitorpamplona.amethyst.model.topNavFeeds.favoriteDvm import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +/** + * Two relay sets, two distinct subscriptions: + * + * - [contentFetches] — for each user-configured content relay, the ids/addresses + * we want to pull (the actual notes the DVM curated). + * - [listenRelays] — the DVM's own publish relays (where it will deliver future + * kind 6300 / 7000 events for this request). + */ class FavoriteDvmTopNavPerRelayFilterSet( - val set: Map, + val contentFetches: Map, + val listenRelays: Set, + val requestId: HexKey?, ) : IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index c8ebc9643..97b01c336 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -1809,8 +1809,8 @@ class AccountViewModel( onReady: (event: Note) -> Unit, ) { launchSigner { - account.requestDVMContentDiscovery(dvmPublicKey) { - onReady(LocalCache.getOrCreateNote(it.id)) + account.requestDVMContentDiscovery(dvmPublicKey) { request, _ -> + onReady(LocalCache.getOrCreateNote(request.id)) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt index 38e38dae6..70a539a9e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt @@ -282,7 +282,20 @@ fun HomeFeeds( accountViewModel: AccountViewModel, nav: INav, ) { - RefresheableBox(feedState, enablePullRefresh) { + val activeFilter by accountViewModel.account.settings.defaultHomeFollowList + .collectAsStateWithLifecycle() + val activeDvm = activeFilter as? TopFilter.FavoriteDvm + + val onRefresh: () -> Unit = { + feedState.invalidateData() + if (activeDvm != null) { + // Swiping down on Home should also re-issue the kind-5300 request so the + // DVM produces a fresh feed, not just re-render whatever's cached. + accountViewModel.refreshFavoriteDvm(activeDvm.address) + } + } + + RefresheableHomeBox(onRefresh, enablePullRefresh) { SaveableFeedContentState(feedState, scrollStateKey) { listState -> RenderFeedContentState( feedContentState = feedState, @@ -291,12 +304,28 @@ fun HomeFeeds( nav = nav, routeForLastRead = routeForLastRead, onLoaded = { FeedLoaded(it, listState, routeForLastRead, liveSection, accountViewModel, nav) }, - onEmpty = { HomeFeedEmpty(feedState::invalidateData) }, + onEmpty = { HomeFeedEmpty(onRefresh) }, ) } } } +@Composable +private fun RefresheableHomeBox( + onRefresh: () -> Unit, + enablePullRefresh: Boolean, + content: @Composable androidx.compose.foundation.layout.BoxScope.() -> Unit, +) { + if (enablePullRefresh) { + RefresheableBox(onRefresh = onRefresh, content = content) + } else { + androidx.compose.foundation.layout.Box( + Modifier.fillMaxSize(), + content = content, + ) + } +} + @OptIn(ExperimentalFoundationApi::class) @Composable fun FeedLoaded( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip90Dvms/FilterHomePostsByDvmIds.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip90Dvms/FilterHomePostsByDvmIds.kt index 644cc1f54..856f6fd9c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip90Dvms/FilterHomePostsByDvmIds.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip90Dvms/FilterHomePostsByDvmIds.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip90Dvms import com.vitorpamplona.amethyst.model.topNavFeeds.favoriteDvm.FavoriteDvmTopNavPerRelayFilter import com.vitorpamplona.amethyst.model.topNavFeeds.favoriteDvm.FavoriteDvmTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -32,22 +33,40 @@ import com.vitorpamplona.quartz.nip90Dvms.status.NIP90StatusEvent /** * Builds relay REQ filters for a favourite-DVM home feed. * - * Three filters are issued per relay: - * - fetch notes whose ids are in the DVM's latest kind-6300 response - * - fetch addressable notes referenced by `a` tags in the DVM's response - * - subscribe to the DVM's future kind 6300 / 7000 events so the filter snapshot - * keeps up to date while this filter is active + * Two distinct subscription kinds with two distinct relay sets: + * + * - **Content fetch** — for each of the user's outbox/proxy relays, request the + * note IDs and addressable references the DVM curated. Notes typically live on + * the user's normal relays, so this is where we fetch them. + * + * - **Response listen** — for each relay the DVM advertised (where it received + * the kind-5300 request and will publish its 6300/7000 reply), subscribe to + * future kind 6300 / 7000 events tagged with the request id. The DVM almost + * never publishes responses on the user's outbox, so listening anywhere else + * would silently miss them. */ fun filterHomePostsByDvmIds( set: FavoriteDvmTopNavPerRelayFilterSet, @Suppress("UNUSED_PARAMETER") since: SincePerRelayMap?, @Suppress("UNUSED_PARAMETER") defaultSince: Long?, -): List = - set.set.flatMap { (relay, filter) -> - buildFiltersFor(relay, filter) +): List { + val out = mutableListOf() + + set.contentFetches.forEach { (relay, filter) -> + out += contentFetchFilters(relay, filter) } -private fun buildFiltersFor( + val requestId = set.requestId + if (requestId != null) { + set.listenRelays.forEach { relay -> + out += responseListenFilter(relay, requestId) + } + } + + return out +} + +private fun contentFetchFilters( relay: NormalizedRelayUrl, filter: FavoriteDvmTopNavPerRelayFilter, ): List { @@ -77,23 +96,22 @@ private fun buildFiltersFor( ) } - val requestId = filter.requestId - if (requestId != null) { - out += - RelayBasedFilter( - relay = relay, - filter = - Filter( - kinds = - listOf( - NIP90ContentDiscoveryResponseEvent.KIND, - NIP90StatusEvent.KIND, - ), - tags = mapOf("e" to listOf(requestId)), - limit = 10, - ), - ) - } - return out } + +private fun responseListenFilter( + relay: NormalizedRelayUrl, + requestId: HexKey, +) = RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = + listOf( + NIP90ContentDiscoveryResponseEvent.KIND, + NIP90StatusEvent.KIND, + ), + tags = mapOf("e" to listOf(requestId)), + limit = 10, + ), +)