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
/**
* 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.
* - [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.
*
* 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
/**
* 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
* favourite set changes.
* favorite set changes.
*/
class AllFavoriteDvmsFeedFlow(
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
* 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
* 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
/**
* 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
* 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
* (where they will deliver future kind 6300 / 7000 events for their requests).
* - [requestIds] — the set of currently-active kind-5300 request ids to listen
* for. A single-DVM filter carries one; the merged "All favourite DVMs"
* filter carries one per favourite DVM.
* for. A single-DVM filter carries one; the merged "All favorite DVMs"
* filter carries one per favorite DVM.
*/
class FavoriteDvmTopNavPerRelayFilterSet(
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.peopleList.PeopleListEvent
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.NIP90ContentDiscoveryRequestEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@@ -182,24 +181,22 @@ class TopNavFilterState(
)
}
// Only DVMs that advertise NIP-90 content discovery (kind 5300) can produce a
// home feed. Hide entries whose AppDefinitionEvent isn't loaded yet OR doesn't
// include kind 5300 so the chip doesn't appear for image-generation, translation,
// search, or other DVM kinds that would never reply.
// Favorites can only be added through FavoriteDvmToggle, which itself checks
// that the AppDefinitionEvent advertises kind 5300. Don't re-check here: on
// cold start the AppDefinitionEvent may not be in cache yet, and dropping
// 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 =
favoriteDvmList.mapNotNull { dvmNote ->
val supports5300 =
(dvmNote.event as? AppDefinitionEvent)
?.includeKind(NIP90ContentDiscoveryRequestEvent.KIND) == true
if (!supports5300) return@mapNotNull null
favoriteDvmList.map { dvmNote ->
FeedDefinition(
TopFilter.FavoriteDvm(dvmNote.address),
FavoriteDvmName(dvmNote),
)
}
// Only show the "All favourite DVMs" meta-chip when there is at least one
// real favourite to merge; otherwise the chip opens to an empty feed.
// Only show the "All favorite DVMs" meta-chip when there is at least one
// real favorite to merge; otherwise the chip opens to an empty feed.
val allFavorites =
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
/**
* 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:
*
+9 -9
View File
@@ -1728,20 +1728,20 @@
<string name="feed_group_locations">Locations</string>
<string name="feed_group_communities">Communities</string>
<string name="feed_group_lists">Lists</string>
<string name="feed_group_dvms">DVMs</string>
<string name="follow_list_all_favorite_dvms">All favourite DVMs</string>
<string name="feed_group_dvms">Feed Algorithms</string>
<string name="follow_list_all_favorite_dvms">All favorite feed algorithms</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="favorite_dvms_title">Favourite DVMs</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_empty">No favourite DVMs yet. Open Discover, tap a content-discovery DVM, and star it to add it here.</string>
<string name="favorite_dvms_title">Favorite Feed Algorithms</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 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_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_payment_required">This DVM requires payment</string>
<string name="dvm_home_status_error">DVM returned an error</string>
<string name="dvm_home_status_payment_required">This feed algorithm requires payment</string>
<string name="dvm_home_status_error">The feed algorithm returned an error</string>
<string name="dvm_home_retry">Retry</string>
<string name="temporary_account">Log off on device lock</string>