mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 11:48:24 +00:00
feat: discovery only shows groups from relays that advertise NIP-29
Stray kind-39000 events published by ordinary users to general relays (e.g. nostr.wine) were surfacing in the Relay Groups discovery feed as joinable groups that have no roster and no chat and can't actually be joined — because those relays don't run NIP-29, they just store the fake metadata like any addressable event. A relay that truly runs NIP-29 rejects user-authored 39xxx, so on such a relay every 39000 is relay-signed and genuine. Gate discovery on that: restrict the per-relay constraint set to relays whose NIP-11 `supported_nips` advertises 29. This is the single point both the match test and the REQ-driven feed read, so non-advertising relays drop out wholesale. The discovery screen warms each candidate relay's NIP-11 and re-invalidates the feed as support resolves (a relay whose NIP-11 lands after its 39000s would otherwise stay hidden until a manual refresh). "My Groups" (the kind-10009 joined list) is unaffected. Trade-off: a relay that runs NIP-29 but doesn't publish NIP-11 (or omits 29 from its list) is hidden from discovery until it advertises. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
This commit is contained in:
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.nip11RelayInfo
|
||||
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
/**
|
||||
* Whether [relay]'s cached NIP-11 document advertises support for [nip] (as a decimal string, e.g.
|
||||
* "29"). Reads only the in-memory cache — it never blocks on a network fetch — so it returns false
|
||||
* for a relay whose NIP-11 hasn't been loaded yet. Callers that need the answer to become true must
|
||||
* warm the document first (e.g. `loadRelayInfo`), then re-evaluate once it resolves.
|
||||
*/
|
||||
fun relayAdvertisesNip(
|
||||
relay: NormalizedRelayUrl,
|
||||
nip: String,
|
||||
): Boolean =
|
||||
Amethyst.instance.nip11Cache
|
||||
.getFromCache(relay)
|
||||
.supported_nips
|
||||
?.any { it == nip } == true
|
||||
|
||||
/** NIP-29 (relay-based groups): the relay must run it for its groups to be real. */
|
||||
fun relayAdvertisesNip29(relay: NormalizedRelayUrl): Boolean = relayAdvertisesNip(relay, "29")
|
||||
+27
-1
@@ -42,7 +42,9 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -51,6 +53,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
@@ -75,6 +78,7 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.dal.relayGroupDiscoveryChannelFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.dal.toGroupConstraints
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupMyJoinedGroupsSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupWarmupSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupsDiscoveryFilterAssemblerSubscription
|
||||
@@ -215,7 +219,29 @@ private fun WatchAccountForRelayGroupDiscovery(
|
||||
val joinedGroups by accountViewModel.account.relayGroupList.liveRelayGroupList
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(listName, perRelay, joinedGroups) {
|
||||
// Discovery only shows groups hosted on relays that actually run NIP-29 (a relay that does
|
||||
// rejects user-authored 39xxx, so its 39000s are all genuine; general relays instead carry stray
|
||||
// fake ones). The feed decides this from each relay's cached NIP-11 `supported_nips`, so warm
|
||||
// the candidate relays here and re-invalidate as support is confirmed — otherwise a relay whose
|
||||
// NIP-11 lands after its 39000s would stay hidden until a manual refresh.
|
||||
val candidateRelays = remember(perRelay) { perRelay.toGroupConstraints().keys }
|
||||
var nip29Relays by remember { mutableStateOf(emptySet<NormalizedRelayUrl>()) }
|
||||
LaunchedEffect(candidateRelays) {
|
||||
val supported = nip29Relays.toMutableSet()
|
||||
candidateRelays.forEach { relay ->
|
||||
Amethyst.instance.nip11Cache.loadRelayInfo(
|
||||
relay = relay,
|
||||
onInfo = { info ->
|
||||
if (info.supported_nips?.any { it == "29" } == true && supported.add(relay)) {
|
||||
nip29Relays = supported.toSet()
|
||||
}
|
||||
},
|
||||
onError = { _, _, _ -> },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(listName, perRelay, joinedGroups, nip29Relays) {
|
||||
feedContentState.checkKeysInvalidateDataAndSendToTop()
|
||||
}
|
||||
}
|
||||
|
||||
+14
-1
@@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.TopFilter
|
||||
import com.vitorpamplona.amethyst.model.filterIntoSet
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.relayAdvertisesNip29
|
||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
@@ -104,7 +105,19 @@ class RelayGroupDiscoveryFeedFilter(
|
||||
joined: Set<GroupId>,
|
||||
): Boolean = channel.groupId in joined || channel.membershipOf(account.userProfile().pubkeyHex).isMember()
|
||||
|
||||
private fun constraints(): Map<NormalizedRelayUrl, GroupDiscoveryConstraint> = account.liveRelayGroupsDiscoveryFollowListsPerRelay.value.toGroupConstraints()
|
||||
/**
|
||||
* The per-relay constraints, restricted to relays that ADVERTISE NIP-29 in their NIP-11
|
||||
* `supported_nips`. A relay that truly runs NIP-29 rejects user-authored 39xxx, so on such a
|
||||
* relay every 39000 is relay-signed and genuine; general relays (nostr.wine, etc.) instead store
|
||||
* stray user-published 39000s that have no roster and can't be joined. Dropping non-advertising
|
||||
* relays here filters out that noise for both the match test and the REQ-driven feed at one
|
||||
* point. Relays whose NIP-11 hasn't loaded yet resolve to false and are warmed by the screen
|
||||
* ([WatchAccountForRelayGroupDiscovery]), which re-invalidates the feed once support is known.
|
||||
*/
|
||||
private fun constraints(): Map<NormalizedRelayUrl, GroupDiscoveryConstraint> =
|
||||
account.liveRelayGroupsDiscoveryFollowListsPerRelay.value
|
||||
.toGroupConstraints()
|
||||
.filterKeys { relayAdvertisesNip29(it) }
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
if (isMine()) return sort(myGroupNotes())
|
||||
|
||||
Reference in New Issue
Block a user