mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 00:16:59 +00:00
refactor: move nickname policy, emoji autocomplete, filters and dialog to commons
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QdvE4LvgkSewJXyFzyyAUY
This commit is contained in:
+6
-15
@@ -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,
|
||||
|
||||
+4
-18
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
+1
-5
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-5
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-4
@@ -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()
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-3
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-5
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-5
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-4
@@ -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()
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-5
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3543,12 +3543,8 @@
|
||||
<string name="playback_actions_dialog_title">Playback</string>
|
||||
<string name="video_quality_auto">Auto</string>
|
||||
|
||||
<!-- Nicknames (NIP-85 contact cards) -->
|
||||
<!-- Nicknames (NIP-85 contact cards); the dialog strings live in commons -->
|
||||
<string name="edit_nickname">Edit nickname</string>
|
||||
<string name="nickname_dialog_title">Nickname</string>
|
||||
<string name="nickname_dialog_explainer">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.</string>
|
||||
<string name="nickname_label">Nickname</string>
|
||||
<string name="nickname_summary_label">Private note about this user</string>
|
||||
|
||||
<!-- LAN cast (Chromecast) feature -->
|
||||
<string name="cast_to_device">Cast to device</string>
|
||||
|
||||
@@ -90,4 +90,15 @@
|
||||
<string name="nsite_source">Source:</string>
|
||||
<string name="nsite_servers">Servers:</string>
|
||||
<string name="nsite_open">Open</string>
|
||||
|
||||
<!-- Custom emoji suggestions (NIP-30) -->
|
||||
<string name="use_direct_url">Use direct URL</string>
|
||||
|
||||
<!-- Nicknames (NIP-85 contact cards) -->
|
||||
<string name="nickname_dialog_title">Nickname</string>
|
||||
<string name="nickname_dialog_explainer">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.</string>
|
||||
<string name="nickname_label">Nickname</string>
|
||||
<string name="nickname_summary_label">Private note about this user</string>
|
||||
<string name="nickname_save">Save</string>
|
||||
<string name="nickname_cancel">Cancel</string>
|
||||
</resources>
|
||||
|
||||
+14
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+17
@@ -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<String> =
|
||||
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) }
|
||||
|
||||
+12
-10
@@ -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),
|
||||
)
|
||||
}
|
||||
+58
-52
@@ -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<TextFieldState?>(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))
|
||||
}
|
||||
},
|
||||
)
|
||||
+28
-1
@@ -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<HexKey>,
|
||||
trustedAccounts: List<HexKey>,
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
Reference in New Issue
Block a user