feat(dvm-favorites): rename user-facing to "Favorite Feed Algorithms" + fix cold-start race

- User-facing rename. All user-visible strings switch from "favourite"
  to "favorite" (American spelling) and from "DVM(s)" to "Feed
  Algorithm(s)": settings entry, spinner group label, empty state,
  banner messages, menu accessibility labels. Code identifiers still
  reference NIP-90's "DVM" protocol name since that's the wire spec.
- Cold-start race. When the app relaunches with a persisted
  TopFilter.FavoriteDvm, the AppDefinitionEvent may not be in cache
  yet. mergeInterests was filtering out any favorite whose
  AppDefinitionEvent didn't yet advertise kind 5300, so the spinner
  didn't include a chip matching the persisted selection — it
  showed "Select an option" while the orchestrator quietly fired the
  RPC with no UI to ground it. Always include every favorite; the
  5300 check at add time (in FavoriteDvmToggle) is enough.
This commit is contained in:
Claude
2026-04-19 16:43:46 +00:00
parent b0f75d4dd4
commit fc284e3cfe
8 changed files with 27 additions and 30 deletions
@@ -45,7 +45,7 @@ import kotlinx.coroutines.sync.withLock
private const val RESPONSE_TIMEOUT_MS = 20_000L private const val RESPONSE_TIMEOUT_MS = 20_000L
/** /**
* Immutable snapshot of a favourite DVM's current request/response state. * Immutable snapshot of a favorite DVM's current request/response state.
* *
* - [requestId] is the id of the most recently published kind-5300 request. * - [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 * - [responseRelays] is the relay set the kind-5300 was sent to — the same set on
@@ -65,7 +65,7 @@ data class FavoriteDvmSnapshot(
) )
/** /**
* Manages the NIP-90 content-discovery RPC cycle for each favourite DVM the user * Manages the NIP-90 content-discovery RPC cycle for each favorite DVM the user
* pins to the top-nav. * pins to the top-nav.
* *
* The orchestrator is lazy: it starts a request/response cycle the first time any * The orchestrator is lazy: it starts a request/response cycle the first time any
@@ -34,9 +34,9 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
/** /**
* Feed flow that merges snapshots from every currently-favourited DVM into a * Feed flow that merges snapshots from every currently-favorited DVM into a
* single [AllFavoriteDvmsTopNavFilter]. Re-wires subscriptions whenever the * single [AllFavoriteDvmsTopNavFilter]. Re-wires subscriptions whenever the
* favourite set changes. * favorite set changes.
*/ */
class AllFavoriteDvmsFeedFlow( class AllFavoriteDvmsFeedFlow(
val favoriteDvmAddresses: StateFlow<Set<Address>>, val favoriteDvmAddresses: StateFlow<Set<Address>>,
@@ -32,7 +32,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
/** /**
* Top-nav filter that unions the latest kind-6300 responses from every currently * Top-nav filter that unions the latest kind-6300 responses from every currently
* favourited DVM. Behaves like [FavoriteDvmTopNavFilter] (pure membership check * favorited DVM. Behaves like [FavoriteDvmTopNavFilter] (pure membership check
* against a snapshot), but the accepted set is the union across N DVMs and the * against a snapshot), but the accepted set is the union across N DVMs and the
* request-id list carries one entry per DVM for the relay-listen subscription. * request-id list carries one entry per DVM for the relay-listen subscription.
*/ */
@@ -32,7 +32,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
/** /**
* Top-nav filter backed by the latest kind-6300 response from a favourite DVM. * Top-nav filter backed by the latest kind-6300 response from a favorite DVM.
* *
* The filter is a pure immutable membership check: [match] accepts a note only if the * The filter is a pure immutable membership check: [match] accepts a note only if the
* DVM's latest response included it. When a new response arrives, a new instance is * DVM's latest response included it. When a new response arrives, a new instance is
@@ -32,8 +32,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
* - [listenRelays] — the union of DVM publish relays across all active DVMs * - [listenRelays] — the union of DVM publish relays across all active DVMs
* (where they will deliver future kind 6300 / 7000 events for their requests). * (where they will deliver future kind 6300 / 7000 events for their requests).
* - [requestIds] — the set of currently-active kind-5300 request ids to listen * - [requestIds] — the set of currently-active kind-5300 request ids to listen
* for. A single-DVM filter carries one; the merged "All favourite DVMs" * for. A single-DVM filter carries one; the merged "All favorite DVMs"
* filter carries one per favourite DVM. * filter carries one per favorite DVM.
*/ */
class FavoriteDvmTopNavPerRelayFilterSet( class FavoriteDvmTopNavPerRelayFilterSet(
val contentFetches: Map<NormalizedRelayUrl, FavoriteDvmTopNavPerRelayFilter>, val contentFetches: Map<NormalizedRelayUrl, FavoriteDvmTopNavPerRelayFilter>,
@@ -35,7 +35,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.NIP90ContentDiscoveryRequestEvent
import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.Log
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -182,24 +181,22 @@ class TopNavFilterState(
) )
} }
// Only DVMs that advertise NIP-90 content discovery (kind 5300) can produce a // Favorites can only be added through FavoriteDvmToggle, which itself checks
// home feed. Hide entries whose AppDefinitionEvent isn't loaded yet OR doesn't // that the AppDefinitionEvent advertises kind 5300. Don't re-check here: on
// include kind 5300 so the chip doesn't appear for image-generation, translation, // cold start the AppDefinitionEvent may not be in cache yet, and dropping
// search, or other DVM kinds that would never reply. // the entry means the persisted TopFilter.FavoriteDvm can't find its chip
// in the spinner (user sees "Select an option" while the banner fires the
// RPC — the bug we had before this change).
val favoriteDvms = val favoriteDvms =
favoriteDvmList.mapNotNull { dvmNote -> favoriteDvmList.map { dvmNote ->
val supports5300 =
(dvmNote.event as? AppDefinitionEvent)
?.includeKind(NIP90ContentDiscoveryRequestEvent.KIND) == true
if (!supports5300) return@mapNotNull null
FeedDefinition( FeedDefinition(
TopFilter.FavoriteDvm(dvmNote.address), TopFilter.FavoriteDvm(dvmNote.address),
FavoriteDvmName(dvmNote), FavoriteDvmName(dvmNote),
) )
} }
// Only show the "All favourite DVMs" meta-chip when there is at least one // Only show the "All favorite DVMs" meta-chip when there is at least one
// real favourite to merge; otherwise the chip opens to an empty feed. // real favorite to merge; otherwise the chip opens to an empty feed.
val allFavorites = val allFavorites =
if (favoriteDvms.isNotEmpty()) listOf(allFavoriteDvmsFollow) else emptyList() if (favoriteDvms.isNotEmpty()) listOf(allFavoriteDvmsFollow) else emptyList()
@@ -31,7 +31,7 @@ import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentD
import com.vitorpamplona.quartz.nip90Dvms.status.NIP90StatusEvent import com.vitorpamplona.quartz.nip90Dvms.status.NIP90StatusEvent
/** /**
* Builds relay REQ filters for a favourite-DVM home feed. * Builds relay REQ filters for a favorite-DVM home feed.
* *
* Two distinct subscription kinds with two distinct relay sets: * Two distinct subscription kinds with two distinct relay sets:
* *
+9 -9
View File
@@ -1728,20 +1728,20 @@
<string name="feed_group_locations">Locations</string> <string name="feed_group_locations">Locations</string>
<string name="feed_group_communities">Communities</string> <string name="feed_group_communities">Communities</string>
<string name="feed_group_lists">Lists</string> <string name="feed_group_lists">Lists</string>
<string name="feed_group_dvms">DVMs</string> <string name="feed_group_dvms">Feed Algorithms</string>
<string name="follow_list_all_favorite_dvms">All favourite DVMs</string> <string name="follow_list_all_favorite_dvms">All favorite feed algorithms</string>
<string name="feed_group_relays">Relays</string> <string name="feed_group_relays">Relays</string>
<string name="add_dvm_to_favorites">Add DVM to favorites</string> <string name="add_dvm_to_favorites">Add feed algorithm to favorites</string>
<string name="remove_dvm_from_favorites">Remove from favorites</string> <string name="remove_dvm_from_favorites">Remove from favorites</string>
<string name="favorite_dvms_title">Favourite DVMs</string> <string name="favorite_dvms_title">Favorite Feed Algorithms</string>
<string name="favorite_dvms_explainer">Content-discovery DVMs you starred here appear as filter chips on the Home feed. Open Discover to add more.</string> <string name="favorite_dvms_explainer">Feed algorithms you starred here appear as filter chips on the Home feed. Open Discover to add more.</string>
<string name="favorite_dvms_empty">No favourite DVMs yet. Open Discover, tap a content-discovery DVM, and star it to add it here.</string> <string name="favorite_dvms_empty">No favorite feed algorithms yet. Open Discover, tap one, and star it to add it here.</string>
<string name="dvm_home_status_requesting">Asking %1$s for a feed…</string> <string name="dvm_home_status_requesting">Asking %1$s for a feed…</string>
<string name="dvm_home_status_requesting_all">Asking your favourite DVMs for feeds…</string> <string name="dvm_home_status_requesting_all">Asking your favorite feed algorithms for feeds…</string>
<string name="dvm_home_status_processing">Processing your feed…</string> <string name="dvm_home_status_processing">Processing your feed…</string>
<string name="dvm_home_status_payment_required">This DVM requires payment</string> <string name="dvm_home_status_payment_required">This feed algorithm requires payment</string>
<string name="dvm_home_status_error">DVM returned an error</string> <string name="dvm_home_status_error">The feed algorithm returned an error</string>
<string name="dvm_home_retry">Retry</string> <string name="dvm_home_retry">Retry</string>
<string name="temporary_account">Log off on device lock</string> <string name="temporary_account">Log off on device lock</string>