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() }