refactor(relays): split the public-chat purpose into the five protocols it held

PUBLIC_CHATS was a catch-all carrying NIP-28 chats, NIP-29 relay groups,
ephemeral chats, geohash chats and live-stream chat under one row labelled
with the NIP-29 name — so the subscription screen could not say where any
of them came from, and the label was wrong for four fifths of what it
counted.

Each is now its own purpose with its own label and explainer. Geohash
cells carry their id too, so location chats list one row per cell and
render as a place name instead of a single opaque group.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-07-30 20:59:48 -04:00
co-authored by Claude Opus 5
parent 1e20e10497
commit b6808959cd
18 changed files with 153 additions and 50 deletions
@@ -49,7 +49,7 @@ fun filterGoalForLiveActivities(
relay = relay,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.LIVE_CHAT,
kinds = listOf(GoalEvent.KIND),
ids = listOf(goalId),
limit = 1,
@@ -59,7 +59,7 @@ fun filterGoalForLiveActivities(
relay = relay,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.LIVE_CHAT,
kinds = listOf(LnZapEvent.KIND, OnchainZapEvent.KIND, Bolt12ZapEvent.KIND),
tags = mapOf("e" to listOf(goalId)),
limit = 200,
@@ -36,7 +36,7 @@ fun filterMessagesToEphemeralChat(
relay = it,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.EPHEMERAL_CHATS,
kinds = listOf(EphemeralChatEvent.KIND),
tags =
if (channel.roomId.id.isBlank()) {
@@ -43,7 +43,10 @@ fun filterMessagesToGeohashChat(
relay = it,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.GEOHASH_CHATS,
// The cell IS the chat's identity, so the screen can list one row per location
// instead of collapsing every joined cell into one unnamed entry.
entityIds = listOf(channel.geohash),
kinds = listOf(GeohashChatEvent.KIND),
tags = mapOf("g" to listOf(channel.geohash)),
limit = 200,
@@ -41,7 +41,7 @@ fun filterMessagesToLiveActivities(
relay = it,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.LIVE_CHAT,
kinds =
listOf(
LiveActivitiesChatMessageEvent.KIND,
@@ -38,7 +38,7 @@ fun filterMyMessagesToEphemeralChat(
relay = it,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.EPHEMERAL_CHATS,
kinds = listOf(EphemeralChatEvent.KIND),
tags =
if (channel.roomId.id.isBlank()) {
@@ -38,7 +38,7 @@ fun filterMyMessagesToLiveActivities(
relay = it,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.LIVE_CHAT,
kinds = listOf(LiveActivitiesChatMessageEvent.KIND),
tags = mapOf("a" to listOfNotNull(channel.address.toValue())),
authors = listOf(pubKey),
@@ -49,8 +49,8 @@ fun filterRelayGroupState(
relays.flatMap {
val floor = since?.get(it)?.time
listOf(
RelayBasedFilter(relay = it, filter = ExplainedFilter(purpose = SubPurpose.PUBLIC_CHATS, kinds = RELAY_GROUP_METADATA_KINDS, tags = scope, since = floor)),
RelayBasedFilter(relay = it, filter = ExplainedFilter(purpose = SubPurpose.PUBLIC_CHATS, kinds = RELAY_GROUP_PIN_KINDS, tags = scope, since = floor)),
RelayBasedFilter(relay = it, filter = ExplainedFilter(purpose = SubPurpose.RELAY_GROUPS, kinds = RELAY_GROUP_METADATA_KINDS, tags = scope, since = floor)),
RelayBasedFilter(relay = it, filter = ExplainedFilter(purpose = SubPurpose.RELAY_GROUPS, kinds = RELAY_GROUP_PIN_KINDS, tags = scope, since = floor)),
)
}
@@ -63,7 +63,7 @@ fun filterRelayGroupState(
if (pinnedIds.isEmpty()) {
emptyList()
} else {
relays.map { RelayBasedFilter(relay = it, filter = ExplainedFilter(purpose = SubPurpose.PUBLIC_CHATS, ids = pinnedIds)) }
relays.map { RelayBasedFilter(relay = it, filter = ExplainedFilter(purpose = SubPurpose.RELAY_GROUPS, ids = pinnedIds)) }
}
return directory + pins
@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManager
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relayClient.AccountScopedQuery
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies.filterRelayGroupState
@@ -35,13 +36,13 @@ import com.vitorpamplona.quartz.nip29RelayGroups.tags.GroupIdTag
/** One on-screen group card's request to warm a single group. */
class RelayGroupCardWarmupQueryState(
val account: Account,
override val account: Account,
val channel: RelayGroupChannel,
/** When true, prefetch only recent content — the caller's screen already streams metadata. */
val contentOnly: Boolean = false,
/** How many recent content events to prefetch ahead of a tap. */
val contentLimit: Int = RELAY_GROUP_WARMUP_LIMIT,
)
) : AccountScopedQuery
/**
* Default number of recent events to pull ahead of a tap — enough to fill the first screen AND drive
@@ -96,7 +97,7 @@ class RelayGroupCardWarmupSubAssembler(
relay = groupId.relayUrl,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.RELAY_GROUPS,
kinds = RELAY_GROUP_CARD_WARMUP_KINDS,
tags = mapOf(GroupIdTag.TAG_NAME to listOf(groupId.id)),
limit = key.contentLimit,
@@ -128,12 +128,12 @@ fun buildRelayGroupLiveStateFilter(groupId: GroupId): List<RelayBasedFilter> =
listOf(
RelayBasedFilter(
relay = groupId.relayUrl,
filter = ExplainedFilter(purpose = SubPurpose.PUBLIC_CHATS, kinds = RELAY_GROUP_METADATA_KINDS, tags = mapOf(GroupIdTag.TAG_NAME to listOf(groupId.id))),
filter = ExplainedFilter(purpose = SubPurpose.RELAY_GROUPS, entityIds = listOf(groupId.id), kinds = RELAY_GROUP_METADATA_KINDS, tags = mapOf(GroupIdTag.TAG_NAME to listOf(groupId.id))),
),
// Pins stay in their own filter for the same reason the directory filters split them out.
RelayBasedFilter(
relay = groupId.relayUrl,
filter = ExplainedFilter(purpose = SubPurpose.PUBLIC_CHATS, kinds = RELAY_GROUP_PIN_KINDS, tags = mapOf(GroupIdTag.TAG_NAME to listOf(groupId.id))),
filter = ExplainedFilter(purpose = SubPurpose.RELAY_GROUPS, entityIds = listOf(groupId.id), kinds = RELAY_GROUP_PIN_KINDS, tags = mapOf(GroupIdTag.TAG_NAME to listOf(groupId.id))),
),
)
@@ -287,8 +287,8 @@ fun buildRelayGroupStateFilters(
val scope = mapOf(D_TAG to ids.distinct())
val since = sinceForRelay(relay)
listOf(
RelayBasedFilter(relay = relay, filter = ExplainedFilter(purpose = SubPurpose.PUBLIC_CHATS, kinds = RELAY_GROUP_METADATA_KINDS, tags = scope, since = since)),
RelayBasedFilter(relay = relay, filter = ExplainedFilter(purpose = SubPurpose.PUBLIC_CHATS, kinds = RELAY_GROUP_PIN_KINDS, tags = scope, since = since)),
RelayBasedFilter(relay = relay, filter = ExplainedFilter(purpose = SubPurpose.RELAY_GROUPS, entityIds = ids.distinct(), kinds = RELAY_GROUP_METADATA_KINDS, tags = scope, since = since)),
RelayBasedFilter(relay = relay, filter = ExplainedFilter(purpose = SubPurpose.RELAY_GROUPS, entityIds = ids.distinct(), kinds = RELAY_GROUP_PIN_KINDS, tags = scope, since = since)),
)
}
@@ -326,7 +326,8 @@ fun buildRelayGroupJoinedChatTailFilter(
relay = groupId.relayUrl,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.RELAY_GROUPS,
entityIds = listOf(groupId.id),
kinds = RELAY_GROUP_ALL_TIMELINE_KINDS,
tags = mapOf(GroupIdTag.TAG_NAME to listOf(groupId.id)),
since = sinceEpoch,
@@ -362,7 +363,8 @@ fun buildRelayGroupPreviewFilter(
relay = groupId.relayUrl,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.RELAY_GROUPS,
entityIds = listOf(groupId.id),
kinds = RELAY_GROUP_ALL_TIMELINE_KINDS,
tags = mapOf(GroupIdTag.TAG_NAME to listOf(groupId.id)),
since = sinceEpoch,
@@ -383,7 +385,8 @@ fun buildRelayGroupAuxFilter(
relay = groupId.relayUrl,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.RELAY_GROUPS,
entityIds = listOf(groupId.id),
kinds = RELAY_GROUP_AUX_KINDS,
tags = mapOf(GroupIdTag.TAG_NAME to listOf(groupId.id)),
since = sinceEpoch,
@@ -400,7 +403,8 @@ fun buildRelayGroupOpenChatTailFilter(
relay = groupId.relayUrl,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.RELAY_GROUPS,
entityIds = listOf(groupId.id),
kinds = RELAY_GROUP_OPEN_TAIL_KINDS,
tags = mapOf(GroupIdTag.TAG_NAME to listOf(groupId.id)),
since = sinceEpoch,
@@ -424,7 +428,8 @@ fun buildRelayGroupHistoryFilters(
relay = relay,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.RELAY_GROUPS,
entityIds = listOf(groupId.id),
kinds = RELAY_GROUP_ALL_TIMELINE_KINDS,
tags = mapOf(GroupIdTag.TAG_NAME to listOf(groupId.id)),
until = until,
@@ -445,7 +450,7 @@ fun buildRelayGroupDirectoryFilter(
relay = relay,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.RELAY_GROUPS,
kinds = RELAY_GROUP_DIRECTORY_KINDS,
limit = RELAY_GROUP_DIRECTORY_LIMIT,
since = sinceEpoch,
@@ -469,7 +474,8 @@ fun buildRelayGroupThreadsHistoryFilters(
relay = relay,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.RELAY_GROUPS,
entityIds = listOf(groupId.id),
kinds = RELAY_GROUP_THREAD_KINDS,
tags = mapOf(GroupIdTag.TAG_NAME to listOf(groupId.id)),
until = until,
@@ -487,7 +493,8 @@ fun buildRelayGroupThreadsFilter(
relay = groupId.relayUrl,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.RELAY_GROUPS,
entityIds = listOf(groupId.id),
kinds = RELAY_GROUP_THREAD_KINDS,
tags = mapOf(GroupIdTag.TAG_NAME to listOf(groupId.id)),
since = sinceEpoch,
@@ -70,7 +70,7 @@ fun filterRelayGroupsByAuthors(
relay = relay,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.RELAY_GROUPS,
authors = authorList,
kinds = listOf(GroupMetadataEvent.KIND),
limit = 200,
@@ -82,7 +82,7 @@ fun filterRelayGroupsByAuthors(
relay = relay,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.RELAY_GROUPS,
kinds = listOf(GroupAdminsEvent.KIND, GroupMembersEvent.KIND),
tags = mapOf(PTag.TAG_NAME to authorList),
limit = 200,
@@ -102,7 +102,7 @@ fun filterRelayGroupsByAuthors(
relay = relay,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.RELAY_GROUPS,
kinds = listOf(GroupMetadataEvent.KIND),
tags = mapOf(DTag.TAG_NAME to rosterGroupIds),
limit = 200,
@@ -41,7 +41,7 @@ fun filterRelayGroupsByGeohashes(
relay = relay,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.RELAY_GROUPS,
kinds = listOf(GroupMetadataEvent.KIND),
tags = mapOf(GeoHashTag.TAG_NAME to geotags.map { it.lowercase() }.sorted()),
limit = 100,
@@ -39,7 +39,7 @@ fun filterRelayGroupsByHashtag(
relay = relay,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.RELAY_GROUPS,
kinds = listOf(GroupMetadataEvent.KIND),
tags = mapOf(HashtagTag.TAG_NAME to hashtags.map { it.lowercase() }),
limit = 200,
@@ -53,7 +53,7 @@ fun filterRelayGroupsDirectory(
relay = relay,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.RELAY_GROUPS,
kinds = RELAY_GROUP_DISCOVERY_KINDS,
limit = 500,
since = since,
@@ -54,7 +54,7 @@ fun filterFollowingEphemeralChats(
relay = it.key,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.EPHEMERAL_CHATS,
kinds = listOf(EphemeralChatEvent.KIND),
tags = mapOf("d" to it.value.sorted()),
limit = 100,
@@ -55,7 +55,10 @@ fun filterFollowingGeohashChats(
relay = relay,
filter =
ExplainedFilter(
purpose = SubPurpose.PUBLIC_CHATS,
purpose = SubPurpose.GEOHASH_CHATS,
// One filter per relay carries every cell that relay serves, so all of them ride
// along and the screen fans them into a row each.
entityIds = cells.sorted(),
kinds = listOf(GeohashChatEvent.KIND),
tags = mapOf("g" to cells),
limit = 100,
@@ -39,31 +39,38 @@ object SubPurposeLabels {
fun labelOf(purpose: SubPurpose): Int =
when (purpose) {
// account — always on
SubPurpose.ACCOUNT_DATA -> R.string.kind_drafts
SubPurpose.PROFILE_METADATA -> R.string.kind_profile
SubPurpose.RELAY_LISTS -> R.string.kind_outbox_relays
SubPurpose.ACCOUNT_DATA -> R.string.relay_purpose_your_account
SubPurpose.PROFILE_METADATA -> R.string.relay_purpose_observing_profiles
SubPurpose.RELAY_LISTS -> R.string.relay_purpose_relay_list_finder
SubPurpose.FOLLOW_LISTS -> R.string.kind_follow_list
SubPurpose.MODERATION -> R.string.kind_reports
SubPurpose.WALLET -> R.string.wallet
SubPurpose.MODERATION -> R.string.relay_purpose_reports_from_follows
SubPurpose.WALLET -> R.string.relay_purpose_your_wallet
SubPurpose.NUTZAP_INBOX -> R.string.relay_purpose_nutzap_inbox
SubPurpose.MINT_DIRECTORY -> R.string.relay_purpose_mint_directory
SubPurpose.NWC -> R.string.relay_purpose_nwc
// messages — always on
SubPurpose.NOTIFICATIONS -> R.string.route_notifications
SubPurpose.DIRECT_MESSAGES -> R.string.route_messages
SubPurpose.PUBLIC_CHATS -> R.string.relay_chat_title
SubPurpose.COMMUNITY_CHATS -> R.string.communities
SubPurpose.DIRECT_MESSAGES -> R.string.relay_purpose_dm_inbox
SubPurpose.PUBLIC_CHATS -> R.string.public_chat
SubPurpose.RELAY_GROUPS -> R.string.relay_purpose_relay_groups
SubPurpose.EPHEMERAL_CHATS -> R.string.relay_purpose_ephemeral_chats
SubPurpose.GEOHASH_CHATS -> R.string.relay_purpose_geohash_chats
SubPurpose.LIVE_CHAT -> R.string.relay_purpose_live_chat
SubPurpose.COMMUNITY_CHATS -> R.string.relay_purpose_community_chats
SubPurpose.ENCRYPTED_GROUPS -> R.string.marmot_group
SubPurpose.LIVE_ROOMS -> R.string.nests
// feeds
SubPurpose.HOME_FEED -> R.string.route_home
SubPurpose.HOME_FEED -> R.string.relay_purpose_home_feed
SubPurpose.DISCOVER_FEED -> R.string.route_discover
SubPurpose.MEDIA_FEED -> R.string.relay_purpose_media
SubPurpose.TAG_FEED -> R.string.relay_purpose_tags
SubPurpose.COMMUNITY_FEED -> R.string.communities
SubPurpose.COMMUNITY_FEED -> R.string.relay_purpose_community_feeds
SubPurpose.TOPIC_FEED -> R.string.relay_purpose_topics
// current screen
SubPurpose.THREAD -> R.string.relay_purpose_thread
SubPurpose.USER_PROFILE -> R.string.kind_profile
SubPurpose.SEARCH -> R.string.relay_purpose_search
SubPurpose.ENGAGEMENT -> R.string.kind_reactions
SubPurpose.ENGAGEMENT -> R.string.relay_purpose_engagement
SubPurpose.REFERENCED_EVENTS -> R.string.relay_purpose_referenced
SubPurpose.ADD_ONS -> R.string.relay_purpose_add_ons
SubPurpose.GAMES -> R.string.route_chess
@@ -95,6 +102,10 @@ object SubPurposeLabels {
SubPurpose.NOTIFICATIONS -> R.string.relay_explain_notifications
SubPurpose.DIRECT_MESSAGES -> R.string.relay_explain_direct_messages
SubPurpose.PUBLIC_CHATS -> R.string.relay_explain_public_chats
SubPurpose.RELAY_GROUPS -> R.string.relay_explain_relay_groups
SubPurpose.EPHEMERAL_CHATS -> R.string.relay_explain_ephemeral_chats
SubPurpose.GEOHASH_CHATS -> R.string.relay_explain_geohash_chats
SubPurpose.LIVE_CHAT -> R.string.relay_explain_live_chat
SubPurpose.COMMUNITY_CHATS -> R.string.relay_explain_community_chats
SubPurpose.ENCRYPTED_GROUPS -> R.string.relay_explain_encrypted_groups
SubPurpose.LIVE_ROOMS -> R.string.relay_explain_live_rooms
@@ -104,7 +115,12 @@ object SubPurposeLabels {
SubPurpose.FOLLOW_LISTS -> R.string.relay_explain_follows
SubPurpose.MODERATION -> R.string.relay_explain_moderation
SubPurpose.WALLET -> R.string.relay_explain_wallet
SubPurpose.NUTZAP_INBOX -> R.string.relay_explain_nutzap_inbox
SubPurpose.MINT_DIRECTORY -> R.string.relay_explain_mint_directory
SubPurpose.NWC -> R.string.relay_explain_nwc
SubPurpose.HOME_FEED -> R.string.relay_explain_home
SubPurpose.ENGAGEMENT -> R.string.relay_explain_engagement
SubPurpose.REFERENCED_EVENTS -> R.string.relay_explain_referenced
else -> null
}
}
+39 -4
View File
@@ -2081,10 +2081,39 @@
<string name="relay_purpose_topics">Topics</string>
<string name="relay_purpose_thread">Conversation</string>
<string name="relay_purpose_search">Search</string>
<string name="relay_purpose_referenced">Quoted posts</string>
<string name="relay_purpose_referenced">Finding Missing Events</string>
<string name="relay_purpose_engagement">Observing Events</string>
<string name="relay_explain_referenced">Fetches events by id that something on your screen refers to but you don\'t have yet — a quote, a reply\'s parent, a thread root.</string>
<string name="relay_explain_engagement">Watches the events currently rendered for new replies, reactions, reposts, zaps and reports, so counts update while you read.</string>
<string name="relay_purpose_add_ons">Add-ons</string>
<string name="relay_purpose_relay_info">Relay info</string>
<string name="relay_purpose_other">Other</string>
<!-- Activity labels, used where the app's own noun for the data type already means something
else to the user: "Outbox Relays" is their own relay list in settings, and "Profile" is
their profile screen. Naming the job avoids an active misreading. -->
<string name="relay_purpose_relay_list_finder">Relay List Finder</string>
<string name="relay_purpose_observing_profiles">Observing Profiles</string>
<string name="relay_purpose_your_account">Your Account\'s data</string>
<string name="relay_purpose_home_feed">Home Feed</string>
<string name="relay_purpose_relay_groups">Relay Groups</string>
<plurals name="active_subs_groups">
<item quantity="one">%1$d group</item>
<item quantity="other">%1$d groups</item>
</plurals>
<string name="relay_purpose_ephemeral_chats">Ephemeral Chats</string>
<string name="relay_purpose_geohash_chats">Location Chats</string>
<string name="relay_purpose_live_chat">Live Stream Chat</string>
<string name="relay_explain_relay_groups">NIP-29 groups you joined. Each group lives on one host relay, so the app connects to every relay that hosts a group of yours.</string>
<string name="relay_explain_ephemeral_chats">Chat rooms that keep no history — messages exist only while you are connected, so these stay subscribed to receive anything at all.</string>
<string name="relay_explain_geohash_chats">Location-based rooms for the areas you follow, asked of the relays that carry them.</string>
<string name="relay_explain_live_chat">Chat and zap goals attached to live streams you have open or follow.</string>
<string name="relay_purpose_dm_inbox">DM Inbox</string>
<string name="relay_purpose_your_wallet">Wallet</string>
<string name="relay_purpose_nutzap_inbox">Nutzap Inbox</string>
<string name="relay_purpose_mint_directory">Mint Directory</string>
<string name="relay_purpose_nwc">Wallet Connect</string>
<string name="relay_purpose_community_chats">Community Chats</string>
<string name="relay_purpose_community_feeds">Community Feeds</string>
<!-- How each subscription actually works, shown on the Active Subscriptions screen.
Describe the real strategy, not the intent — these are read by people trying to explain
a relay count they think is too high. -->
@@ -2098,9 +2127,13 @@
<string name="relay_explain_profiles">Profiles of the people currently on screen.</string>
<string name="relay_explain_relay_lists">Finds which relays each person publishes to, so their posts can be fetched from the right place.</string>
<string name="relay_explain_follows">Follow lists, used to build your feed and your web of trust.</string>
<string name="relay_explain_moderation">Reports written by people you follow, asked of each relay those people post to.</string>
<string name="relay_explain_wallet">Your mints, wallet state and incoming nutzaps.</string>
<string name="active_subs_title">Active Subscriptions</string>
<string name="relay_explain_moderation">Reports your follows wrote about the profiles currently on your screen, asked of each relay those follows post to.</string>
<string name="relay_purpose_reports_from_follows">Reports from Follows</string>
<string name="relay_explain_wallet">Your own wallet events, read back from the relays you published them to.</string>
<string name="relay_explain_nutzap_inbox">Listens on your nutzap relays plus your inbox and DM relays, so a payment cannot slip past.</string>
<string name="relay_explain_mint_directory">Looks across relays for which mints exist and which ones people recommend.</string>
<string name="relay_explain_nwc">Notifications from your connected wallet.</string>
<string name="active_subs_title">Active Relay Subscriptions</string>
<!-- Two countable nouns, so two plurals composed at the call site rather than one string with
two %d in it: filter and relay decline independently in Slavic/Baltic/Semitic languages. -->
<plurals name="active_subs_filters">
@@ -2118,6 +2151,8 @@
<string name="active_subs_pair">%1$s \u00b7 %2$s</string>
<string name="active_subs_unattributed">Not attributed to an account</string>
<string name="active_subs_no_entity">All</string>
<string name="active_subs_share">%1$d%% of all</string>
<string name="active_subs_search_keywords">subscriptions filters relays requests reqs connections why diagnostics</string>
<string name="relay_explain_home">Posts by people you follow, read from the relays each of them publishes to.</string>
<string name="always_on_notif_connecting">Connecting to inbox relays\u2026</string>
<string name="always_on_notif_setting_title">Always-on notification service</string>
@@ -69,9 +69,28 @@ enum class SubPurpose(
/** Reports (kind 1984), mute lists, spam and ban state. */
MODERATION(SubPurposeGroup.ACCOUNT, runsInBackground = true),
/** Cashu wallet, nutzaps, mints and NWC. */
/**
* Your own NIP-60 wallet state, read back from your outbox relays. Narrow by construction.
*
* Split from the wider wallet jobs because they answer different questions: this is "restore my
* wallet", [NUTZAP_INBOX] is "listen everywhere someone might pay me", and [MINT_DIRECTORY] is
* "what mints exist". Lumping them made a handful of relays look like a dozen.
*/
WALLET(SubPurposeGroup.ACCOUNT, runsInBackground = true),
/**
* Inbound nutzaps (kind 9321). Deliberately the **union** of the NIP-61 `10019` relays, the
* NIP-65 inbox and the DM relays — per the assembler, "so a nutzap can't slip past us". That
* union is why this is wide, and it is a choice rather than an accident.
*/
NUTZAP_INBOX(SubPurposeGroup.ACCOUNT, runsInBackground = true),
/** Mint announcements and recommendations — a discovery query, so it fans out. */
MINT_DIRECTORY(SubPurposeGroup.ACCOUNT),
/** Nostr Wallet Connect notifications, on the wallet-connect relay. */
NWC(SubPurposeGroup.ACCOUNT, runsInBackground = true),
// ---- things addressed to me: always on ---------------------------------
/** Mentions, reactions, reposts and zaps addressed to me (`#p` = me). Inbox relays. */
@@ -80,9 +99,28 @@ enum class SubPurpose(
/** NIP-17 gift wraps and NIP-04 legacy DMs. DM relays. */
DIRECT_MESSAGES(SubPurposeGroup.MESSAGES, runsInBackground = true),
/** NIP-28/29 public channels and relay groups. */
/**
* NIP-28 public channels only.
*
* This was a catch-all that also carried NIP-29 relay groups, ephemeral chats, geohash chats and
* live-stream chat — five protocols in one row labelled with the NIP-29 name, so the subscription
* screen could not say where any of them came from. They are separate below, each named and
* counted on its own.
*/
PUBLIC_CHATS(SubPurposeGroup.MESSAGES, runsInBackground = true),
/** NIP-29 relay groups. Each group is keyed by (id, host relay). */
RELAY_GROUPS(SubPurposeGroup.MESSAGES, runsInBackground = true),
/** NIP-C7 ephemeral chats — no history is stored, so only live delivery exists. */
EPHEMERAL_CHATS(SubPurposeGroup.MESSAGES, runsInBackground = true),
/** Location-scoped chat rooms. */
GEOHASH_CHATS(SubPurposeGroup.MESSAGES, runsInBackground = true),
/** Chat and zap goals attached to live streams. */
LIVE_CHAT(SubPurposeGroup.MESSAGES, runsInBackground = true),
/** Concord and Buzz community planes. */
COMMUNITY_CHATS(SubPurposeGroup.MESSAGES, runsInBackground = true),