From e8ecf429a20438c7531faa279cbadd6fbfe8103f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 21:20:36 +0000 Subject: [PATCH] refactor: move nickname policy, emoji autocomplete, filters and dialog to commons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generalizes the remaining platform-bound pieces of the nickname feature so the desktop app can adopt it: - ContactCardsState.displayNameFlow/cachedDisplayName own the NIP-81 render policy (petname over profile name over npub); the Android observeUserName is now a thin wrapper around it - EmojiSuggestionState.autocompleteInto completes the word under the cursor and closes the list — replaces the insert+reset pair copy-pasted in the 8 composer ViewModels and the nickname dialog - the kind:30382 filter builders move to commons relayClient/assemblers (cards about targets from trusted accounts, and the account's own cards by author), shared by the user watcher and the login subscription - ShowEmojiSuggestionList moves to commons nip30CustomEmojis/ui using coil and commons string resources - EditNicknameDialog moves to commons nip85TrustedAssertions/ui: it takes the ContactCardsState and an onSave callback, so each front end only wires its own publish path and menu entry; dialog strings move to commons resources Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01QdvE4LvgkSewJXyFzyyAUY --- .../FilterAccountInfoAndListsFromKey.kt | 21 +--- .../reqCommand/user/UserObservers.kt | 22 +--- .../user/watchers/UserCardsSubAssembler.kt | 1 + .../nip22Comments/CommentPostViewModel.kt | 6 +- .../nip22Comments/GenericCommentPostScreen.kt | 2 +- .../privateDM/send/ChatNewMessageViewModel.kt | 6 +- .../chats/privateDM/send/NewGroupDMScreen.kt | 2 +- .../send/PrivateMessageEditFieldRow.kt | 2 +- .../send/ChannelNewMessageViewModel.kt | 5 +- .../chats/publicChannels/send/EditFieldRow.kt | 2 +- .../nip23LongForm/LongFormPostScreen.kt | 2 +- .../nip23LongForm/LongFormPostViewModel.kt | 4 +- .../nip99Classifieds/NewProductScreen.kt | 2 +- .../nip99Classifieds/NewProductViewModel.kt | 6 +- .../loggedIn/home/ShortNotePostScreen.kt | 2 +- .../loggedIn/home/ShortNotePostViewModel.kt | 6 +- .../nests/room/chat/NestEditFieldRow.kt | 2 +- .../room/chat/NestNewMessageViewModel.kt | 5 +- .../publicMessages/NewPublicMessageScreen.kt | 2 +- .../NewPublicMessageViewModel.kt | 6 +- .../profile/header/UserProfileDropDownMenu.kt | 7 +- amethyst/src/main/res/values/strings.xml | 6 +- .../composeResources/values/strings.xml | 11 ++ .../nip30CustomEmojis/EmojiSuggestionState.kt | 14 +++ .../ContactCardsState.kt | 17 +++ .../ui}/ShowEmojiSuggestionList.kt | 22 ++-- .../ui}/EditNicknameDialog.kt | 110 +++++++++--------- .../assemblers/ContactCardFilters.kt | 29 ++++- 28 files changed, 175 insertions(+), 147 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip30CustomEmojis/ui}/ShowEmojiSuggestionList.kt (86%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip85TrustedAssertions/ui}/EditNicknameDialog.kt (55%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterContactCardsToKey.kt => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/ContactCardFilters.kt (69%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt index 967bbd4f45..673e6cba60 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata +import com.vitorpamplona.amethyst.commons.relayClient.assemblers.filterContactCardsByAuthorInTheRelay import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState.Companion.APP_SPECIFIC_DATA_D_TAG import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent @@ -49,7 +50,6 @@ import com.vitorpamplona.quartz.nip61Nutzaps.info.NutzapInfoEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent -import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent @@ -94,12 +94,6 @@ val AccountInfoAndListsFromKeyKinds2 = NutzapInfoEvent.KIND, ) -// The account's own kind:30382 contact cards (nicknames/petnames for other -// users, NIP-44 encrypted). Addressable: one card per target user, so this is a -// collection rather than a single replaceable — kept out of the small-limit -// filters above and given its own filter with a larger limit. -val AccountContactCardKinds = listOf(ContactCardEvent.KIND) - val AmethystMetadataKinds = listOf(AppSpecificDataEvent.KIND) val AmethystMetadataTagMapFilter = mapOf("d" to listOf(APP_SPECIFIC_DATA_D_TAG)) @@ -131,15 +125,12 @@ fun filterAccountInfoAndListsFromKey( since = since, ), ), - RelayBasedFilter( + // The account's own kind:30382 contact cards (nicknames, NIP-44 encrypted). + // Addressable — one card per target user — hence its own larger-limit filter. + filterContactCardsByAuthorInTheRelay( relay = relay, - filter = - Filter( - kinds = AccountContactCardKinds, - authors = listOf(pubkey), - limit = 500, - since = since, - ), + author = pubkey, + since = since, ), RelayBasedFilter( relay = relay, 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 beb8cb9182..26dcbe7c94 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 @@ -61,25 +61,11 @@ fun observeUserName( // Subscribe in the relay for changes in the metadata of this user. UserFinderFilterAssemblerSubscription(user, accountViewModel) - val flow = - remember(user) { - combine( - user.metadata().flow, - accountViewModel.account.contactCards.petNameFlow(user), - ) { info, petName -> - petName?.petName ?: info?.info?.bestName() ?: user.pubkeyDisplayHex() - }.distinctUntilChanged() - } - - val initialName = - remember(user) { - accountViewModel.account.contactCards - .cachedPetName(user) - ?.petName ?: user.toBestDisplayName() - } + val contactCards = accountViewModel.account.contactCards + val flow = remember(user) { contactCards.displayNameFlow(user) } // Subscribe in the LocalCache for changes that arrive in the device - return flow.collectAsStateWithLifecycle(initialName) + return flow.collectAsStateWithLifecycle(remember(user) { contactCards.cachedDisplayName(user) }) } /** @@ -323,7 +309,7 @@ fun observeUserBookmarkCount( val combined = remember(user) { - kotlinx.coroutines.flow.combine(newFlow, oldFlow) { newCount, oldCount -> + combine(newFlow, oldFlow) { newCount, oldCount -> newCount + oldCount } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt index 19474b6c48..bd297744e8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserCardsSubAssembler.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers import com.vitorpamplona.amethyst.commons.model.toHexSet +import com.vitorpamplona.amethyst.commons.relayClient.assemblers.filterContactCardsToTargetKeysFromTrustedAccountsInTheRelay import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index b4d9c028ff..0943395636 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -920,13 +920,9 @@ open class CommentPostViewModel : } open fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) { - val wordToInsert = ":${item.code}:" - - message.replaceCurrentWord(wordToInsert) + emojiSuggestions?.autocompleteInto(message, item) urlPreviews.update(message.text.toString()) - emojiSuggestions?.reset() - draftTag.newVersion() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index 8d4497e18d..ae2542cbcc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -52,6 +52,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel 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.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles @@ -65,7 +66,6 @@ import com.vitorpamplona.amethyst.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton -import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDateButton import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index fc6ed14ce5..2d92030b7b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -764,13 +764,9 @@ class ChatNewMessageViewModel : } fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) { - val wordToInsert = ":${item.code}:" - - message.replaceCurrentWord(wordToInsert) + emojiSuggestions?.autocompleteInto(message, item) urlPreviews.update(message.text.toString()) - emojiSuggestions?.reset() - draftTag.newVersion() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt index 36987b6160..8941be093f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt @@ -71,6 +71,7 @@ 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 +import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlVideo @@ -93,7 +94,6 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton -import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDateButton import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt index a089688b21..fc64efe0ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt @@ -51,6 +51,7 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation @@ -63,7 +64,6 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture -import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.ui.note.showCount import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index eab9d48766..377b424bbb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -678,10 +678,7 @@ open class ChannelNewMessageViewModel : } open fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) { - val wordToInsert = ":${item.code}:" - message.replaceCurrentWord(wordToInsert) - - emojiSuggestions?.reset() + emojiSuggestions?.autocompleteInto(message, item) draftTag.newVersion() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt index e9f150b50a..f23c35fce7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation @@ -44,7 +45,6 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.navigation.navs.INav -import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt index 95cdc5577c..93c8a6b0ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt @@ -83,6 +83,7 @@ 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.model.EmptyTagList +import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation @@ -99,7 +100,6 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton -import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDateButton import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt index cb6d7bdc80..47686de456 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt @@ -692,9 +692,7 @@ class LongFormPostViewModel : } fun autocompleteWithEmoji(item: EmojiMedia) { - val wordToInsert = ":${item.code}:" - message.replaceCurrentWord(wordToInsert) - emojiSuggestions?.reset() + emojiSuggestions?.autocompleteInto(message, item) draftTag.newVersion() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index 6f4fd20c97..9f32382d97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery @@ -58,7 +59,6 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton -import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDateButton import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index f4206076ac..6e7554d7dd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -558,13 +558,9 @@ open class NewProductViewModel : } open fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) { - val wordToInsert = ":${item.code}:" - - message.replaceCurrentWord(wordToInsert) + emojiSuggestions?.autocompleteInto(message, item) urlPreviews.update(message.text.toString()) - emojiSuggestions?.reset() - draftTag.newVersion() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 534927c0f1..7453377467 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -76,6 +76,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel 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.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS @@ -99,7 +100,6 @@ import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.creators.aihelp.AiWritingHelpPanel import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton -import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDateButton import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 03e6d4c30b..c7ef8c9626 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -1610,13 +1610,9 @@ open class ShortNotePostViewModel : } open fun autocompleteWithEmoji(item: EmojiMedia) { - val wordToInsert = ":${item.code}:" - - message.replaceCurrentWord(wordToInsert) + emojiSuggestions?.autocompleteInto(message, item) urlPreviews.update(message.text.toString()) - emojiSuggestions?.reset() - draftTag.newVersion() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt index f6fa574b3e..88a8f1e51f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt @@ -36,13 +36,13 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.navigation.navs.INav -import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt index dd164b0792..63a55b94c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt @@ -548,10 +548,7 @@ open class NestNewMessageViewModel : } open fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) { - val wordToInsert = ":${item.code}:" - message.replaceCurrentWord(wordToInsert) - - emojiSuggestions?.reset() + emojiSuggestions?.autocompleteInto(message, item) draftTag.newVersion() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt index 06d46689e6..f10bd8db4e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt @@ -55,6 +55,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation @@ -69,7 +70,6 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton -import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDateButton import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index ff9988c888..68ffbf6006 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -621,13 +621,9 @@ class NewPublicMessageViewModel : } fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) { - val wordToInsert = ":${item.code}:" - - message.replaceCurrentWord(wordToInsert) + emojiSuggestions?.autocompleteInto(message, item) urlPreviews.update(message.text.toString()) - emojiSuggestions?.reset() - draftTag.newVersion() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt index 71e9ac8a38..258f7dbf82 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt @@ -29,11 +29,13 @@ import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalContext import com.vitorpamplona.amethyst.R 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.ui.components.M3ActionDialog import com.vitorpamplona.amethyst.ui.components.M3ActionRow import com.vitorpamplona.amethyst.ui.components.M3ActionSection import com.vitorpamplona.amethyst.ui.components.util.setText +import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.externalLinkForUser import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes @@ -50,10 +52,13 @@ fun UserProfileDropDownMenu( val isNicknameDialogOpen = remember { mutableStateOf(false) } if (isNicknameDialogOpen.value) { + // keeps the account's selected emoji packs loaded for the : autocomplete + WatchAndLoadMyEmojiList(accountViewModel) EditNicknameDialog( user = user, + contactCards = accountViewModel.account.contactCards, + onSave = { petName, summary -> accountViewModel.updateContactCardPetName(user, petName, summary) }, onDismiss = { isNicknameDialogOpen.value = false }, - accountViewModel = accountViewModel, ) } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index c6f381f16e..8c0897ec31 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -3543,12 +3543,8 @@ Playback Auto - + Edit nickname - Nickname - Shown to you instead of this user\'s name, everywhere in the app. It\'s saved encrypted in your contact card: only you can read it. Type : to use your custom emojis. - Nickname - Private note about this user Cast to device diff --git a/commons/src/commonMain/composeResources/values/strings.xml b/commons/src/commonMain/composeResources/values/strings.xml index f7cbf115a9..e50c831382 100644 --- a/commons/src/commonMain/composeResources/values/strings.xml +++ b/commons/src/commonMain/composeResources/values/strings.xml @@ -90,4 +90,15 @@ Source: Servers: Open + + + Use direct URL + + + Nickname + Shown to you instead of this user\'s name, everywhere in the app. It\'s saved encrypted in your contact card: only you can read it. Type : to use your custom emojis. + Nickname + Private note about this user + Save + Cancel diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/EmojiSuggestionState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/EmojiSuggestionState.kt index 08dec98379..0f5e457153 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/EmojiSuggestionState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/EmojiSuggestionState.kt @@ -20,7 +20,9 @@ */ package com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.flow.Flow @@ -66,4 +68,16 @@ class EmojiSuggestionState( } } } + + /** + * Completes the word under the cursor in [field] with the selected emoji's + * `:shortcode:` and closes the suggestion list. + */ + fun autocompleteInto( + field: TextFieldState, + item: EmojiPackState.EmojiMedia, + ) { + field.replaceCurrentWord(":${item.code}:") + reset() + } } 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 9ed53fc670..25b370002d 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 @@ -33,6 +33,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.IO import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf @@ -112,6 +113,22 @@ class ContactCardsState( return decryptionCache.cachedPetNameWithEmojis(card) } + /** + * The name to render for [target], per the NIP-81 policy: the nickname the + * account gave them wins over the profile's own display name, falling back + * to the short npub when neither exists. + */ + fun displayNameFlow(target: User): Flow = + combine( + target.metadata().flow, + petNameFlow(target), + ) { info, petName -> + petName?.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() + suspend fun petName(target: HexKey): String? = getCard(target)?.let { decryptionCache.petName(it) } suspend fun summary(target: HexKey): String? = getCard(target)?.let { decryptionCache.summary(it) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/ShowEmojiSuggestionList.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip30CustomEmojis/ui/ShowEmojiSuggestionList.kt similarity index 86% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/ShowEmojiSuggestionList.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip30CustomEmojis/ui/ShowEmojiSuggestionList.kt index 953a044747..61d0048193 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/ShowEmojiSuggestionList.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip30CustomEmojis/ui/ShowEmojiSuggestionList.kt @@ -18,7 +18,7 @@ * 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.note.creators.emojiSuggestions +package com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement.spacedBy @@ -39,15 +39,17 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage -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.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState -import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.DividerThickness -import com.vitorpamplona.amethyst.ui.theme.Size10dp -import com.vitorpamplona.amethyst.ui.theme.Size40Modifier +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.use_direct_url +import org.jetbrains.compose.resources.stringResource + +private val DividerThickness = 0.25.dp +private val RowSpacing = 10.dp +private val EmojiSize = Modifier.size(40.dp) @Composable fun ShowEmojiSuggestionList( @@ -73,23 +75,23 @@ fun ShowEmojiSuggestionList( bottom = 10.dp, ), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = spacedBy(Size10dp), + horizontalArrangement = spacedBy(RowSpacing), ) { AsyncImage( it.link, contentDescription = it.code, - modifier = Size40Modifier, + modifier = EmojiSize, ) Text(it.code, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f)) IconButton( - modifier = Size40Modifier, + modifier = EmojiSize, onClick = { onFullSize(it) }, ) { Icon( symbol = MaterialSymbols.OpenInFull, - contentDescription = stringRes(R.string.use_direct_url), + contentDescription = stringResource(Res.string.use_direct_url), modifier = Modifier.size(20.dp), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/EditNicknameDialog.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip85TrustedAssertions/ui/EditNicknameDialog.kt similarity index 55% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/EditNicknameDialog.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip85TrustedAssertions/ui/EditNicknameDialog.kt index 54f1a469a3..b24c98cbb5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/EditNicknameDialog.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip85TrustedAssertions/ui/EditNicknameDialog.kt @@ -18,35 +18,41 @@ * 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 +package com.vitorpamplona.amethyst.commons.nip85TrustedAssertions.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState +import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.ContactCardsState +import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.nickname_cancel +import com.vitorpamplona.amethyst.commons.resources.nickname_dialog_explainer +import com.vitorpamplona.amethyst.commons.resources.nickname_dialog_title +import com.vitorpamplona.amethyst.commons.resources.nickname_label +import com.vitorpamplona.amethyst.commons.resources.nickname_save +import com.vitorpamplona.amethyst.commons.resources.nickname_summary_label import com.vitorpamplona.amethyst.commons.ui.text.currentWord -import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField -import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList -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.placeholderText +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.stringResource /** * Edits the nickname (petname) and private note (summary) the account keeps for @@ -55,79 +61,82 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText * * Typing `:` offers the account's NIP-30 custom emojis; the mappings for any * shortcode used are embedded (also encrypted) so the nickname renders with them. + * + * Shared by every front end: the caller supplies the account's [contactCards] + * and publishes the result in [onSave] (e.g. through its outbox relays). On + * Android, compose `WatchAndLoadMyEmojiList` alongside so the emoji packs load. */ @Composable fun EditNicknameDialog( user: User, + contactCards: ContactCardsState, + onSave: (petName: String?, summary: String?) -> Unit, onDismiss: () -> Unit, - accountViewModel: AccountViewModel, ) { val nickname = rememberTextFieldState() val summary = rememberTextFieldState() - val emojiSuggestions = remember(accountViewModel) { EmojiSuggestionState(accountViewModel.account.emoji) } - // which field the emoji autocomplete should insert into + val emojiSuggestions = remember(contactCards) { EmojiSuggestionState(contactCards.emojiPacks) } + // which field the emoji autocomplete should insert into: the last one edited val emojiTarget = remember { mutableStateOf(null) } - // keeps the account's selected emoji packs loaded while the dialog is open - WatchAndLoadMyEmojiList(accountViewModel) - // Prefill with the card's current encrypted values, if any. Decryption can // be slow on external signers, so don't clobber anything already typed. LaunchedEffect(user) { - accountViewModel.account.contactCards + contactCards .petName(user.pubkeyHex) ?.takeIf { nickname.text.isEmpty() } ?.let { nickname.setTextAndPlaceCursorAtEnd(it) } - accountViewModel.account.contactCards + contactCards .summary(user.pubkeyHex) ?.takeIf { summary.text.isEmpty() } ?.let { summary.setTextAndPlaceCursorAtEnd(it) } } - fun watchEmojiIn(field: TextFieldState) { - emojiTarget.value = field - if (field.selection.collapsed) { - emojiSuggestions.processCurrentWord(field.currentWord()) + // Feed the word under the cursor of the last-edited field to the autocomplete. + LaunchedEffect(nickname, summary) { + fun watch(field: TextFieldState) { + emojiTarget.value = field + if (field.selection.collapsed) { + emojiSuggestions.processCurrentWord(field.currentWord()) + } } + launch { snapshotFlow { nickname.text }.collect { watch(nickname) } } + launch { snapshotFlow { summary.text }.collect { watch(summary) } } } AlertDialog( onDismissRequest = onDismiss, title = { - Text(text = stringRes(R.string.nickname_dialog_title)) + Text(text = stringResource(Res.string.nickname_dialog_title)) }, text = { Column( verticalArrangement = Arrangement.spacedBy(10.dp), ) { Text( - text = stringRes(R.string.nickname_dialog_explainer), - color = MaterialTheme.colorScheme.placeholderText, + text = stringResource(Res.string.nickname_dialog_explainer), + color = MaterialTheme.colorScheme.onSurfaceVariant, ) - ThinPaddingTextField( + OutlinedTextField( state = nickname, - onTextChanged = { watchEmojiIn(nickname) }, - singleLine = true, + lineLimits = TextFieldLineLimits.SingleLine, label = { - Text(text = stringRes(R.string.nickname_label)) + Text(text = stringResource(Res.string.nickname_label)) }, ) - ThinPaddingTextField( + OutlinedTextField( state = summary, - onTextChanged = { watchEmojiIn(summary) }, label = { - Text(text = stringRes(R.string.nickname_summary_label)) + Text(text = stringResource(Res.string.nickname_summary_label)) }, ) ShowEmojiSuggestionList( emojiSuggestions, - onSelect = { - emojiTarget.value?.replaceCurrentWord(":${it.code}:") - emojiSuggestions.reset() + onSelect = { emoji -> + emojiTarget.value?.let { emojiSuggestions.autocompleteInto(it, emoji) } }, - onFullSize = { - emojiTarget.value?.replaceCurrentWord(":${it.code}:") - emojiSuggestions.reset() + onFullSize = { emoji -> + emojiTarget.value?.let { emojiSuggestions.autocompleteInto(it, emoji) } }, modifier = Modifier.heightIn(max = 200.dp), ) @@ -136,30 +145,27 @@ fun EditNicknameDialog( confirmButton = { Button( onClick = { - accountViewModel.updateContactCardPetName( - user = user, - petName = - nickname.text - .toString() - .trim() - .ifBlank { null }, - summary = - summary.text - .toString() - .trim() - .ifBlank { null }, + onSave( + nickname.text + .toString() + .trim() + .ifBlank { null }, + summary.text + .toString() + .trim() + .ifBlank { null }, ) onDismiss() }, ) { - Text(stringRes(R.string.save)) + Text(stringResource(Res.string.nickname_save)) } }, dismissButton = { Button( onClick = onDismiss, ) { - Text(stringRes(R.string.cancel)) + Text(stringResource(Res.string.nickname_cancel)) } }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterContactCardsToKey.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/ContactCardFilters.kt similarity index 69% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterContactCardsToKey.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/ContactCardFilters.kt index 8013013f99..f75e6819f2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterContactCardsToKey.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/ContactCardFilters.kt @@ -18,7 +18,7 @@ * 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.service.relayClient.reqCommand.user.watchers +package com.vitorpamplona.amethyst.commons.relayClient.assemblers import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter @@ -28,6 +28,11 @@ import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent val ContactCardKindList = listOf(ContactCardEvent.KIND) +/** + * Kind:30382 cards *about* [targets], written by [trustedAccounts] (the account + * itself plus its WoT trust providers). Fetches nicknames and scores for the + * users currently on screen. + */ fun filterContactCardsToTargetKeysFromTrustedAccountsInTheRelay( targets: Set, trustedAccounts: List, @@ -46,3 +51,25 @@ fun filterContactCardsToTargetKeysFromTrustedAccountsInTheRelay( ), ) } + +/** + * Every kind:30382 card *written by* [author] — the account's own nicknames — + * for the bulk download at login from the account's relays. Addressable events: + * one card per target user, hence the larger limit. + */ +fun filterContactCardsByAuthorInTheRelay( + relay: NormalizedRelayUrl, + author: HexKey, + since: Long?, + limit: Int = 500, +): RelayBasedFilter = + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = ContactCardKindList, + authors = listOf(author), + limit = limit, + since = since, + ), + )