feat: rank conversation participants first in @-mention suggestions

Adds an optional priorityPubkeys supplier to UserSuggestionState that
stable-sorts search results so the current conversation's participants
appear before network-wide matches (ranking only, never filters).

Wired per chat context:
- MLS/Marmot groups: live MLS member list
- NIP-17 DMs/groups: the room's users
- Public chats (NIP-28): authors who have posted in the channel
- Nests audio rooms: MeetingSpaceEvent participants + host

https://claude.ai/code/session_013NWdjCSegsf2FYSPPANX3n
This commit is contained in:
Claude
2026-06-10 16:34:09 +00:00
parent 84e91833c4
commit bf0fa0c57e
5 changed files with 48 additions and 5 deletions

View File

@@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.logTime
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrlOrNull
import com.vitorpamplona.quartz.nip05DnsIdentifiers.INip05Client
@@ -58,10 +59,20 @@ val userUriPrefixes =
DualCase("nostr:nprofile"),
)
/**
* Drives the @-mention autocomplete dropdown: searches the local cache,
* relays, and NIP-05 identifiers for the word currently being typed.
*
* [priorityPubkeys] is a live supplier of pubkeys to rank first in the
* results — pass the current conversation's participants (NIP-17 room
* users, public-chat authors, MLS group members, …) so they beat
* network-wide matches. Ranking only; it never filters anyone out.
*/
@Stable
class UserSuggestionState(
val account: Account,
val nip05Client: INip05Client,
val priorityPubkeys: () -> Set<HexKey> = { emptySet() },
) {
val invalidations = MutableStateFlow(0)
val currentWord = MutableStateFlow("")
@@ -158,7 +169,13 @@ class UserSuggestionState(
}
if (prefix != null) {
logTime("UserSuggestionState Search $prefix version $version") {
account.cache.findUsersStartingWith(prefix, account)
val found = account.cache.findUsersStartingWith(prefix, account)
val priority = priorityPubkeys()
if (priority.isEmpty()) {
found
} else {
found.sortedByDescending { it.pubkeyHex in priority }
}
}
} else {
emptyList()

View File

@@ -190,7 +190,12 @@ fun MarmotGroupMessageComposer(
val userSuggestions =
remember(nostrGroupId) {
UserSuggestionState(accountViewModel.account, accountViewModel.nip05ClientBuilder())
val group = accountViewModel.account.marmotGroupList.getOrCreateGroup(nostrGroupId)
UserSuggestionState(
accountViewModel.account,
accountViewModel.nip05ClientBuilder(),
priorityPubkeys = { group.members.value.mapTo(mutableSetOf()) { it.pubkey } },
)
}
DisposableEffect(nostrGroupId) {

View File

@@ -261,7 +261,12 @@ class ChatNewMessageViewModel :
this.canAddZapRaiser = hasLnAddress()
this.userSuggestions?.reset()
this.userSuggestions = UserSuggestionState(accountVM.account, accountVM.nip05ClientBuilder())
this.userSuggestions =
UserSuggestionState(
accountVM.account,
accountVM.nip05ClientBuilder(),
priorityPubkeys = { room.value?.users ?: emptySet() },
)
this.emojiSuggestions?.reset()
this.emojiSuggestions = EmojiSuggestionState(accountVM.account)

View File

@@ -188,7 +188,14 @@ open class ChannelNewMessageViewModel :
this.canAddZapRaiser = hasLnAddress()
this.userSuggestions?.reset()
this.userSuggestions = UserSuggestionState(accountVM.account, accountVM.nip05ClientBuilder())
this.userSuggestions =
UserSuggestionState(
accountVM.account,
accountVM.nip05ClientBuilder(),
priorityPubkeys = {
channel?.participatingAuthors(0)?.mapTo(mutableSetOf()) { it.pubkeyHex } ?: emptySet()
},
)
this.emojiSuggestions?.reset()
this.emojiSuggestions = EmojiSuggestionState(accountVM.account)

View File

@@ -194,7 +194,16 @@ open class NestNewMessageViewModel :
this.canAddZapRaiser = hasLnAddress()
this.userSuggestions?.reset()
this.userSuggestions = UserSuggestionState(accountVM.account, accountVM.nip05ClientBuilder())
this.userSuggestions =
UserSuggestionState(
accountVM.account,
accountVM.nip05ClientBuilder(),
priorityPubkeys = {
(room?.event as? MeetingSpaceEvent)?.let { space ->
space.participantKeys().toMutableSet().apply { add(space.pubKey) }
} ?: emptySet()
},
)
this.emojiSuggestions?.reset()
this.emojiSuggestions = EmojiSuggestionState(accountVM.account)