From b76b37744ea23e070c1e48df0dd3b9bee36f465e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 23:33:05 +0000 Subject: [PATCH] feat: nickname card on the user profile, above the real display name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The profile header no longer replaces the big display name with the petname. Instead, when the account nicknamed the user (or kept a private note about them), an outlined card renders above it — petname, divider, private summary — with the standard Lock private marker in its top-right corner, since both fields live NIP-44 encrypted in the account's contact card. Tapping the card opens the shared nickname editor. The profile's own display name stays fully visible underneath. Feeds, chats and mentions keep rendering the petname instead of the display name. To carry the summary into the UI, the commons PetName holder generalizes to Nickname(petName?, summary?, tags), built when either field exists — so a note-only card (no petname) now shows on the profile too, while the name override everywhere else keys strictly off petName. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01QdvE4LvgkSewJXyFzyyAUY --- .../reqCommand/user/UserObservers.kt | 20 +-- .../amethyst/ui/components/ClickableRoute.kt | 9 +- .../amethyst/ui/components/RichTextViewer.kt | 9 +- .../amethyst/ui/note/UsernameDisplay.kt | 9 +- .../loggedIn/chats/feed/DrawAuthorInfo.kt | 11 +- .../profile/header/DrawAdditionalInfo.kt | 13 +- .../profile/header/UserNicknameCard.kt | 125 ++++++++++++++++++ amethyst/src/main/res/values/strings.xml | 1 + .../ContactCardDecryptionCache.kt | 16 ++- .../ContactCardsState.kt | 23 ++-- .../{PetName.kt => Nickname.kt} | 21 ++- 11 files changed, 198 insertions(+), 59 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserNicknameCard.kt rename commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/{PetName.kt => Nickname.kt} (66%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt index 26dcbe7c94..82a1de7a66 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt @@ -28,7 +28,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.model.nip01Core.UserInfo import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel -import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.PetName +import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.Nickname import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.NoteState @@ -69,21 +69,21 @@ fun observeUserName( } /** - * The nickname (NIP-85 petname) the logged-in account gave this user through - * its own contact card, decrypted from the card's content, with the card's - * tags so `:shortcode:` custom emojis resolve. Null when the account never - * nicknamed this user. Per the spec, when present it should be rendered - * instead of the user's display name. + * The nickname (NIP-85 petname + private summary) the logged-in account gave + * this user through its own contact card, decrypted from the card's content, + * with the card's tags so `:shortcode:` custom emojis resolve. Null when the + * account never nicknamed this user. Per the spec, the petname should be + * rendered instead of the user's display name. */ @Composable -fun observeUserPetName( +fun observeUserNickname( user: User, accountViewModel: AccountViewModel, -): State { +): State { val contactCards = accountViewModel.account.contactCards - val flow = remember(user) { contactCards.petNameFlow(user) } + val flow = remember(user) { contactCards.nicknameFlow(user) } - return flow.collectAsStateWithLifecycle(remember(user) { contactCards.cachedPetName(user) }) + return flow.collectAsStateWithLifecycle(remember(user) { contactCards.cachedNickname(user) }) } @OptIn(ExperimentalCoroutinesApi::class) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index b5a61c19bb..4423e58a9c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -60,7 +60,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPetName +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserNickname import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor @@ -299,15 +299,16 @@ fun RenderUserAsClickableText( nav: INav, ) { val userState by observeUserInfo(baseUser, accountViewModel) - val petName by observeUserPetName(baseUser, accountViewModel) + val nickname by observeUserNickname(baseUser, accountViewModel) + val petName = nickname?.petName CreateClickableTextWithEmoji( - clickablePart = "@" + (petName?.petName ?: userState?.info?.bestName() ?: baseUser.pubkeyDisplayHex()), + clickablePart = "@" + (petName ?: userState?.info?.bestName() ?: baseUser.pubkeyDisplayHex()), suffix = additionalChars?.ifBlank { null }, maxLines = 1, route = remember(baseUser) { routeFor(baseUser) }, nav = nav, - tags = petName?.tags ?: userState?.tags ?: EmptyTagList, + tags = (if (petName != null) nickname?.tags else userState?.tags) ?: EmptyTagList, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 56ce3b6411..54e13bb3d9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -105,7 +105,7 @@ import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon import com.vitorpamplona.amethyst.service.CachedRichTextParser import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPetName +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserNickname import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.openBlossomUriAsIntent import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown @@ -1011,16 +1011,17 @@ private fun DisplayUserFromTag( nav: INav, ) { val meta by observeUserInfo(baseUser, accountViewModel) - val petName by observeUserPetName(baseUser, accountViewModel) + val nickname by observeUserNickname(baseUser, accountViewModel) + val petName = nickname?.petName CrossfadeIfEnabled(targetState = meta, label = "DisplayUserFromTag", accountViewModel = accountViewModel) { Row { CreateClickableTextWithEmoji( - clickablePart = remember(meta, petName) { petName?.petName ?: it?.info?.bestName() ?: baseUser.pubkeyDisplayHex() }, + clickablePart = remember(meta, petName) { petName ?: it?.info?.bestName() ?: baseUser.pubkeyDisplayHex() }, maxLines = 1, route = remember(baseUser) { routeFor(baseUser) }, nav = nav, - tags = petName?.tags ?: it?.tags, + tags = if (petName != null) nickname?.tags else it?.tags, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt index c36934bbb5..f8fdd1f072 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt @@ -40,7 +40,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPetName +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserNickname import com.vitorpamplona.amethyst.service.tts.TextToSpeechHelper import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji @@ -106,14 +106,15 @@ fun UsernameDisplay( accountViewModel: AccountViewModel, ) { val userMetadata by observeUserInfo(baseUser, accountViewModel) - val petName by observeUserPetName(baseUser, accountViewModel) + val nickname by observeUserNickname(baseUser, accountViewModel) CrossfadeIfEnabled(targetState = userMetadata, modifier = weight, label = "UsernameDisplay", accountViewModel = accountViewModel) { // the account's own nickname for this user wins over the user's metadata; // its custom emojis resolve against the contact card's tags, not the profile's - val name = petName?.petName ?: it?.info?.bestName() + val petName = nickname?.petName + val name = petName ?: it?.info?.bestName() if (name != null) { - UserDisplay(name, petName?.tags ?: it?.tags, weight, fontWeight, textColor, textAlign) + UserDisplay(name, if (petName != null) nickname?.tags else it?.tags, weight, fontWeight, textColor, textAlign) } else { NPubDisplay(baseUser, weight, fontWeight, textColor, textAlign) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt index c05a352e98..35a6fcae36 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt @@ -29,7 +29,7 @@ import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPetName +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserNickname import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.FollowingIcon @@ -59,14 +59,15 @@ private fun WatchAndDisplayUser( nav: INav, ) { val userState by observeUserInfo(author, accountViewModel) - val petName by observeUserPetName(author, accountViewModel) + val nickname by observeUserNickname(author, accountViewModel) + val petName = nickname?.petName UserDisplayNameLayout( picture = { InnerUserPicture( userHex = author.pubkeyHex, userPicture = userState?.info?.picture, - userName = petName?.petName ?: userState?.info?.bestName(), + userName = petName ?: userState?.info?.bestName(), size = Size20dp, modifier = Modifier, accountViewModel = accountViewModel, @@ -83,8 +84,8 @@ private fun WatchAndDisplayUser( name = { if (userState != null) { CreateTextWithEmoji( - text = petName?.petName ?: userState?.info?.bestName() ?: author.pubkeyDisplayHex(), - tags = petName?.tags ?: userState?.tags ?: EmptyTagList, + text = petName ?: userState?.info?.bestName() ?: author.pubkeyDisplayHex(), + tags = (if (petName != null) nickname?.tags else userState?.tags) ?: EmptyTagList, maxLines = 1, fontWeight = FontWeight.Bold, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt index 33c17b342a..17b801c28a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt @@ -56,7 +56,6 @@ import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05State import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPetName import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.util.LongPressCopyText @@ -107,17 +106,17 @@ fun DrawAdditionalInfo( val scope = rememberCoroutineScope() val identities by externalIdentities.identities.collectAsStateWithLifecycle() - val petName by observeUserPetName(baseUser, accountViewModel) - - // the nickname the account gave this user wins over the profile's own name; - // the "@name" line below keeps the real handle visible for disambiguation - val displayName = petName?.petName ?: user.info.bestName() + val displayName = user.info.bestName() val ui = accountViewModel.settings.uiSettingsFlow val showBadges by ui.showProfileBadges.collectAsStateWithLifecycle() val showAppRecommendations by ui.showProfileAppRecommendations.collectAsStateWithLifecycle() Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = SpacedBy3dp) { + // the nickname the account gave this user, on top of (not replacing) + // the profile's own display name below + UserNicknameCard(baseUser, accountViewModel) + if (displayName != null) { Row( verticalAlignment = Alignment.CenterVertically, @@ -125,7 +124,7 @@ fun DrawAdditionalInfo( ) { CreateTextWithEmoji( text = displayName, - tags = petName?.tags ?: user.tags, + tags = user.tags, fontWeight = FontWeight.Bold, fontSize = 22.sp, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserNicknameCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserNicknameCard.kt new file mode 100644 index 0000000000..cf678ff575 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserNicknameCard.kt @@ -0,0 +1,125 @@ +/* + * 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.ui.screen.loggedIn.profile.header + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.nip85TrustedAssertions.ui.EditNicknameDialog +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserNickname +import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji +import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.placeholderText + +/** + * The nickname (petname) and private note the account keeps for [baseUser], + * shown above the profile's own display name without replacing it. The lock + * marks it as private — both fields live NIP-44 encrypted in the account's + * contact card. Tapping the card opens the editor. + */ +@Composable +fun UserNicknameCard( + baseUser: User, + accountViewModel: AccountViewModel, +) { + val nickname by observeUserNickname(baseUser, accountViewModel) + val card = nickname ?: return + + val isEditDialogOpen = remember { mutableStateOf(false) } + + if (isEditDialogOpen.value) { + // keeps the account's selected emoji packs loaded for the : autocomplete + WatchAndLoadMyEmojiList(accountViewModel) + EditNicknameDialog( + user = baseUser, + contactCards = accountViewModel.account.contactCards, + onSave = { petName, summary -> accountViewModel.updateContactCardPetName(baseUser, petName, summary) }, + onDismiss = { isEditDialogOpen.value = false }, + ) + } + + OutlinedCard( + onClick = { isEditDialogOpen.value = true }, + modifier = Modifier.fillMaxWidth().padding(top = 7.dp), + ) { + Box(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 10.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + card.petName?.let { + CreateTextWithEmoji( + text = it, + tags = card.tags, + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + ) + } + + if (card.petName != null && card.summary != null) { + HorizontalDivider(thickness = DividerThickness) + } + + card.summary?.let { + CreateTextWithEmoji( + text = it, + tags = card.tags, + color = MaterialTheme.colorScheme.placeholderText, + fontSize = 14.sp, + ) + } + } + + Icon( + symbol = MaterialSymbols.Lock, + contentDescription = stringRes(R.string.nickname_private), + tint = MaterialTheme.colorScheme.placeholderText, + modifier = + Modifier + .align(Alignment.TopEnd) + .padding(top = 8.dp, end = 8.dp) + .size(14.dp), + ) + } + } +} diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 8c0897ec31..5ec3ebfd23 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -3545,6 +3545,7 @@ Edit nickname + Only visible to you Cast to device diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/ContactCardDecryptionCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/ContactCardDecryptionCache.kt index f47c2204c2..e6e5477791 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/ContactCardDecryptionCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/ContactCardDecryptionCache.kt @@ -44,20 +44,22 @@ class ContactCardDecryptionCache( suspend fun summary(event: ContactCardEvent) = cachedPrivateCards.mergeTagList(event).summary() /** - * The petname plus the card's full decrypted tag list, so renderers can - * resolve the NIP-30 `emoji` mappings stored alongside it. + * The decrypted petname and summary plus the card's full decrypted tag list, + * so renderers can resolve the NIP-30 `emoji` mappings stored alongside them. */ - suspend fun petNameWithEmojis(event: ContactCardEvent): PetName? = cachedPrivateCards.mergeTagList(event).toPetName() + suspend fun nickname(event: ContactCardEvent): Nickname? = cachedPrivateCards.mergeTagList(event).toNickname() /** * Synchronous variant that only reads an already-decrypted card, for use as * the immediate value of UI flows. Returns null until the suspend path has * decrypted the card once. */ - fun cachedPetNameWithEmojis(event: ContactCardEvent): PetName? = cachedPrivateCards.mergeTagListPrecached(event).toPetName() + fun cachedNickname(event: ContactCardEvent): Nickname? = cachedPrivateCards.mergeTagListPrecached(event).toNickname() - private fun TagArray.toPetName(): PetName? { - val name = petName() ?: return null - return PetName(name, toImmutableListOfLists()) + private fun TagArray.toNickname(): Nickname? { + val name = petName() + val summary = summary() + if (name == null && summary == null) return null + return Nickname(name, summary, toImmutableListOfLists()) } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/ContactCardsState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/ContactCardsState.kt index 25b370002d..f643429c5c 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/ContactCardsState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/ContactCardsState.kt @@ -87,22 +87,23 @@ class ContactCardsState( } /** - * The petname the account gave [target], decrypted from the card's content, - * along with the card's tags so `:shortcode:` custom emojis resolve. + * The nickname (petname + private summary) the account gave [target], + * decrypted from the card's content, along with the card's tags so + * `:shortcode:` custom emojis resolve. */ @OptIn(ExperimentalCoroutinesApi::class) - fun petNameFlow(target: User): Flow = + fun nicknameFlow(target: User): Flow = myCardFlow(target) - .mapLatest { card -> card?.let { decryptionCache.petNameWithEmojis(it) } } + .mapLatest { card -> card?.let { decryptionCache.nickname(it) } } .distinctUntilChanged() .flowOn(Dispatchers.IO) /** - * Synchronously returns the petname for [target] when its card is already + * Synchronously returns the nickname for [target] when its card is already * decrypted (or has none to decrypt). Cheap enough for initial values of UI * flows: a map read plus the decryption cache lookup, no crypto. */ - fun cachedPetName(target: User): PetName? { + fun cachedNickname(target: User): Nickname? { val card = target .cardsOrNull() @@ -110,7 +111,7 @@ class ContactCardsState( ?.value ?.get(accountUser) ?.event as? ContactCardEvent ?: return null - return decryptionCache.cachedPetNameWithEmojis(card) + return decryptionCache.cachedNickname(card) } /** @@ -121,13 +122,13 @@ class ContactCardsState( fun displayNameFlow(target: User): Flow = combine( target.metadata().flow, - petNameFlow(target), - ) { info, petName -> - petName?.petName ?: info?.info?.bestName() ?: target.pubkeyDisplayHex() + nicknameFlow(target), + ) { info, nickname -> + nickname?.petName ?: info?.info?.bestName() ?: target.pubkeyDisplayHex() }.distinctUntilChanged() /** Synchronous first value for [displayNameFlow], from already-decrypted data. */ - fun cachedDisplayName(target: User): String = cachedPetName(target)?.petName ?: target.toBestDisplayName() + fun cachedDisplayName(target: User): String = cachedNickname(target)?.petName ?: target.toBestDisplayName() suspend fun petName(target: HexKey): String? = getCard(target)?.let { decryptionCache.petName(it) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/PetName.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/Nickname.kt similarity index 66% rename from commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/PetName.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/Nickname.kt index 1cabf8e564..1d678013a8 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/PetName.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/Nickname.kt @@ -24,18 +24,25 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists /** - * The nickname the account gave a user, together with the card's decrypted tag - * list so renderers can resolve any NIP-30 `:shortcode:` custom emojis the - * petname uses (the `emoji` mappings live encrypted next to the petname). + * The decrypted private fields of the account's contact card about a user: the + * nickname (petname) and the private note (summary), plus the card's decrypted + * tag list so renderers can resolve any NIP-30 `:shortcode:` custom emojis they + * use (the `emoji` mappings live encrypted next to them). Only built when at + * least one of the two fields is present. */ @Immutable -class PetName( - val petName: String, +class Nickname( + val petName: String?, + val summary: String?, val tags: ImmutableListOfLists, ) { // content equality so flow distinctUntilChanged() dedupes re-decryptions of // the same card (ImmutableListOfLists itself compares by identity) - override fun equals(other: Any?): Boolean = other is PetName && petName == other.petName && tags.lists.contentDeepEquals(other.tags.lists) + override fun equals(other: Any?): Boolean = + other is Nickname && + petName == other.petName && + summary == other.summary && + tags.lists.contentDeepEquals(other.tags.lists) - override fun hashCode(): Int = 31 * petName.hashCode() + tags.contentHash() + override fun hashCode(): Int = 31 * (31 * petName.hashCode() + summary.hashCode()) + tags.contentHash() }