feat(desktop): WoT badges on search-result person cards

Extends the WoT trust indicator to the Search screen's person-picker
results, matching the badges already shown on note-card avatars.

- `UserSearchCard` (commons) gains an optional
  `badge: @Composable (BoxScope.() -> Unit)? = null` param, forwarded
  to its embedded `UserAvatar` (which has the slot from the WoT PR).
  Default null → no visual change for callers that don't opt in;
  Android search screens continue to render as before.
- `SearchResultsList` (desktopApp) inlines the score-lookup gates in
  a small `wotBadgeFor(pubkey)` helper and passes the badge lambda at
  both person-result call sites (main list + expandable overflow).

Same visibility rules as the note-card avatar badges:
score > 0, past the 2 s startup readiness gate, and pubkey not in
`LocalSpamExemptKeys` (self / already-followed).
This commit is contained in:
nrobi144
2026-07-06 09:22:51 +03:00
parent ce3536ddf7
commit afa1a3b652
2 changed files with 41 additions and 0 deletions
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.commons.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
@@ -46,12 +47,17 @@ import org.jetbrains.compose.resources.stringResource
/**
* A card displaying user search result with avatar, name, and nip05/pubkey.
* Shared between Android and Desktop search screens.
*
* @param badge Optional overlay drawn on top of the avatar (bottom-right
* by convention). Used by Desktop for the WoT trust-score chip; Android
* call sites leave it null. Forwarded to [UserAvatar].
*/
@Composable
fun UserSearchCard(
user: User,
onClick: () -> Unit,
modifier: Modifier = Modifier,
badge: @Composable (BoxScope.() -> Unit)? = null,
) {
Card(
modifier =
@@ -73,6 +79,7 @@ fun UserSearchCard(
pictureUrl = user.profilePicture(),
size = 40.dp,
contentDescription = stringResource(Res.string.accessibility_user_avatar),
badge = badge,
)
Column(modifier = Modifier.weight(1f)) {
@@ -55,12 +55,16 @@ import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.moderation.LocalSpamExemptKeys
import com.vitorpamplona.amethyst.commons.search.AdvancedSearchBarState
import com.vitorpamplona.amethyst.commons.search.SearchSortOrder
import com.vitorpamplona.amethyst.commons.ui.components.UserSearchCard
import com.vitorpamplona.amethyst.commons.wot.LocalWoTReady
import com.vitorpamplona.amethyst.commons.wot.LocalWoTService
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
import com.vitorpamplona.amethyst.desktop.ui.note.NoteCard
import com.vitorpamplona.amethyst.desktop.ui.note.SpamCheckedNoteRender
import com.vitorpamplona.amethyst.desktop.ui.note.WoTBadge
import com.vitorpamplona.amethyst.desktop.ui.rememberDisplayData
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
@@ -116,6 +120,7 @@ fun SearchResultsList(
UserSearchCard(
user = user,
onClick = { onNavigateToProfile(user.pubkeyHex) },
badge = wotBadgeFor(user.pubkeyHex),
)
}
if (people.size > 5) {
@@ -126,6 +131,7 @@ fun SearchResultsList(
UserSearchCard(
user = user,
onClick = { onNavigateToProfile(user.pubkeyHex) },
badge = wotBadgeFor(user.pubkeyHex),
)
}
}
@@ -286,6 +292,34 @@ fun SearchResultsList(
}
}
/**
* Returns a WoT-badge lambda for the given pubkey, or null when the
* badge should be hidden. Same gates as [WoTBadgedAvatar]:
* - WoT service is provided
* - initial batch fetch has finished (or 2 s startup timeout fired)
* - the pubkey is not exempt (self or already followed)
* - the score is > 0
* Inlined here (rather than wrapped in a new composable) because it's
* only used at the two SearchResultsList person-result call sites.
*/
@Composable
private fun wotBadgeFor(userHex: String): (@Composable androidx.compose.foundation.layout.BoxScope.() -> Unit)? {
val service = LocalWoTService.current
val ready = LocalWoTReady.current
val exempt = LocalSpamExemptKeys.current
val score =
if (service != null && ready && userHex !in exempt) {
service.scores[userHex] ?: 0
} else {
0
}
return if (score > 0) {
{ WoTBadge(count = score, modifier = Modifier.align(Alignment.BottomEnd)) }
} else {
null
}
}
@Composable
private fun SortableHeader(
title: String,