fix(home): route DVM listen subscription to DVM relays + wire pull-to-refresh
Two issues from the first pass: 1. Response routing. The kind-5300 request goes to the DVM's own inbox/used relays, but `filterHomePostsByDvmIds` was subscribing for kind 6300/7000 on the user's outbox/proxy relays. Most DVMs only publish their replies on their own relays, so the listen subscription would silently miss every response. Fix: `Account.requestDVMContentDiscovery` now exposes the relay set the request was sent to, the orchestrator stores it on the snapshot as `responseRelays`, and `FavoriteDvmTopNavPerRelayFilterSet` carries two distinct relay sets — `contentFetches` (user's outbox, for the actual notes) and `listenRelays` (DVM's relays, for the 6300/7000 reply subscription). `filterHomePostsByDvmIds` issues each on the right relay. 2. Pull-to-refresh. Swiping down on Home only re-rendered the cached feed. When a `TopFilter.FavoriteDvm` is active it now also calls `orchestrator.refresh(addr)` so a fresh kind-5300 request is published to the DVM.
This commit is contained in:
@@ -2357,7 +2357,7 @@ class Account(
|
||||
|
||||
suspend fun requestDVMContentDiscovery(
|
||||
dvmPublicKey: User,
|
||||
onReady: (event: NIP90ContentDiscoveryRequestEvent) -> Unit,
|
||||
onReady: (event: NIP90ContentDiscoveryRequestEvent, relays: Set<NormalizedRelayUrl>) -> Unit,
|
||||
) {
|
||||
val relays = nip65RelayList.inboxFlow.value.toSet()
|
||||
val request = signer.sign<NIP90ContentDiscoveryRequestEvent>(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)
|
||||
}
|
||||
|
||||
+7
-1
@@ -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<NormalizedRelayUrl> = emptySet(),
|
||||
val ids: Set<HexKey> = emptySet(),
|
||||
val addresses: Set<String> = 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,
|
||||
|
||||
+4
-3
@@ -43,12 +43,13 @@ class FavoriteDvmFeedFlow(
|
||||
|
||||
private fun buildFilter(
|
||||
snapshot: com.vitorpamplona.amethyst.model.dvms.FavoriteDvmSnapshot,
|
||||
relays: Set<NormalizedRelayUrl>,
|
||||
contentRelays: Set<NormalizedRelayUrl>,
|
||||
) = 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<IFeedTopNavFilter>) {
|
||||
|
||||
+11
-9
@@ -43,7 +43,8 @@ class FavoriteDvmTopNavFilter(
|
||||
val dvmAddress: Address,
|
||||
val acceptedIds: Set<HexKey>,
|
||||
val acceptedAddresses: Set<String>,
|
||||
val relayList: Set<NormalizedRelayUrl>,
|
||||
val contentRelays: Set<NormalizedRelayUrl>,
|
||||
val listenRelays: Set<NormalizedRelayUrl>,
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
-2
@@ -26,8 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
@Immutable
|
||||
class FavoriteDvmTopNavPerRelayFilter(
|
||||
val dvmPubkey: HexKey,
|
||||
val requestId: HexKey?,
|
||||
val ids: Set<HexKey>,
|
||||
val addresses: Set<String>,
|
||||
) : IFeedTopNavPerRelayFilter
|
||||
|
||||
+12
-1
@@ -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<NormalizedRelayUrl, FavoriteDvmTopNavPerRelayFilter>,
|
||||
val contentFetches: Map<NormalizedRelayUrl, FavoriteDvmTopNavPerRelayFilter>,
|
||||
val listenRelays: Set<NormalizedRelayUrl>,
|
||||
val requestId: HexKey?,
|
||||
) : IFeedTopNavPerRelayFilterSet
|
||||
|
||||
+2
-2
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+31
-2
@@ -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(
|
||||
|
||||
+45
-27
@@ -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<RelayBasedFilter> =
|
||||
set.set.flatMap { (relay, filter) ->
|
||||
buildFiltersFor(relay, filter)
|
||||
): List<RelayBasedFilter> {
|
||||
val out = mutableListOf<RelayBasedFilter>()
|
||||
|
||||
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<RelayBasedFilter> {
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user