diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/OwnedEmojiPacksState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/OwnedEmojiPacksState.kt index b6f2be289..ad85b687e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/OwnedEmojiPacksState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/OwnedEmojiPacksState.kt @@ -43,6 +43,8 @@ import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent import com.vitorpamplona.quartz.nip51Lists.remove import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag +import com.vitorpamplona.quartz.nip51Lists.tags.NameTag +import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -179,8 +181,8 @@ class OwnedEmojiPacksState( val template = packEvent.update { - remove(com.vitorpamplona.quartz.nip51Lists.tags.NameTag.TAG_NAME) - remove(com.vitorpamplona.quartz.nip51Lists.tags.TitleTag.TAG_NAME) + remove(NameTag.TAG_NAME) + remove(TitleTag.TAG_NAME) remove(DescriptionTag.TAG_NAME) remove(ImageTag.TAG_NAME) title(newTitle) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/browse/BrowseEmojiSetsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/browse/BrowseEmojiSetsScreen.kt index 3bdb01131..1686b5a37 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/browse/BrowseEmojiSetsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/browse/BrowseEmojiSetsScreen.kt @@ -184,11 +184,13 @@ private fun BrowsedEmojiPackCard( event.taggedEmojis().map { it.url } } val coverImage = remember(event) { event.image() } + val author = remember(note) { note.author?.toBestDisplayName() } EmojiPackCard( title = title, emojiUrls = emojiUrls, coverImage = coverImage, + author = author, onClick = onClick, modifier = modifier, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/browse/datasource/BrowseEmojiSetsSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/browse/datasource/BrowseEmojiSetsSubAssembler.kt index 0862fedce..3dadb080d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/browse/datasource/BrowseEmojiSetsSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/browse/datasource/BrowseEmojiSetsSubAssembler.kt @@ -78,7 +78,7 @@ class BrowseEmojiSetsSubAssembler( invalidateFilters() } }, - key.account.scope.launch(Dispatchers.IO) { + key.scope.launch(Dispatchers.IO) { key.feedStates.browseEmojiSetsFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest { invalidateFilters() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/common/EmojiPackCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/common/EmojiPackCard.kt index b8ac366e2..68364b630 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/common/EmojiPackCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/common/EmojiPackCard.kt @@ -58,6 +58,7 @@ fun EmojiPackCard( title: String, emojiUrls: List, coverImage: String? = null, + author: String? = null, onClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -81,6 +82,16 @@ fun EmojiPackCard( maxLines = 1, overflow = TextOverflow.Ellipsis, ) + if (!author.isNullOrBlank()) { + Spacer(Modifier.height(2.dp)) + Text( + text = stringRes(R.string.my_emoji_list_by_author, author), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } Spacer(Modifier.height(2.dp)) Text( text = stringRes(R.string.emoji_pack_count, emojiCount), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/AddEmojiDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/AddEmojiDialog.kt index 790251a8a..733fe36c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/AddEmojiDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/AddEmojiDialog.kt @@ -22,18 +22,20 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.display import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.LockOpen import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button -import androidx.compose.material3.FilterChip import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf @@ -41,6 +43,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp @@ -52,16 +55,6 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag -/** - * Dialog for adding a custom emoji to an owned emoji pack (NIP-30 kind 30030). - * - * The [onConfirm] callback receives the new [EmojiUrlTag] alongside an `isPrivate` - * flag: when `true`, the caller stores the entry in the event's encrypted - * `.content` (NIP-51 private tags) rather than as a public tag. Private emojis - * are surfaced to the pack owner end-to-end (autocomplete + reaction menu) via - * asynchronous decryption — see - * [com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState.mergePackWithPrivate]. - */ @Composable fun AddEmojiDialog( viewModel: EmojiPackViewModel, @@ -141,17 +134,25 @@ fun AddEmojiDialog( label = { Text(stringRes(R.string.emoji_pack_address_label)) }, ) Spacer(DoubleVertSpacer) - FilterChip( - selected = isPrivate, - onClick = { isPrivate = !isPrivate }, - label = { Text(stringRes(R.string.emoji_private_toggle)) }, - leadingIcon = { - Icon( - imageVector = if (isPrivate) Icons.Default.Lock else Icons.Default.LockOpen, - contentDescription = null, - ) - }, - ) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = if (isPrivate) Icons.Default.Lock else Icons.Default.LockOpen, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(Modifier.width(12.dp)) + Text( + text = stringRes(R.string.emoji_private_toggle), + modifier = Modifier.weight(1f), + ) + Switch( + checked = isPrivate, + onCheckedChange = { isPrivate = it }, + ) + } Spacer(DoubleVertSpacer) Text( text = @@ -171,7 +172,10 @@ fun AddEmojiDialog( Button( enabled = canConfirm, onClick = { - val parsedAddress = packAddressText.trim().takeIf { it.isNotEmpty() }?.let { Address.parse(it) } + val parsedAddress = + packAddressText.trim().takeIf { it.isNotEmpty() }?.let { + runCatching { Address.parse(it) }.getOrNull() + } onConfirm( EmojiUrlTag(code = shortcode, url = url.trim(), emojiSet = parsedAddress), isPrivate, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/EmojiPackScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/EmojiPackScreen.kt index 07ce91323..ebf5343ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/EmojiPackScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/EmojiPackScreen.kt @@ -37,6 +37,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.outlined.Delete import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -158,6 +159,14 @@ private fun EmojiPackScreenView( ).consumeWindowInsets(padding), ) { pack?.let { currentPack -> + if (currentPack.publicEmojis.isNotEmpty() || currentPack.privateEmojis.isNotEmpty()) { + Text( + text = stringRes(R.string.emoji_long_press_hint), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + ) + } EmojiGrid( pack = currentPack, onLongPress = { emoji, isPrivate -> pendingDelete = EmojiDeleteTarget(emoji, isPrivate) }, @@ -187,7 +196,7 @@ private fun EmojiPackScreenView( ) { M3ActionSection { M3ActionRow( - icon = Icons.Outlined.Add, + icon = Icons.Outlined.Delete, text = stringRes(R.string.quick_action_delete), isDestructive = true, ) { @@ -259,7 +268,7 @@ private fun EmojiCell( model = emoji.url, contentDescription = if (isPrivate) "${emoji.code} ($privateLabel)" else emoji.code, modifier = Size35Modifier, - contentScale = ContentScale.Crop, + contentScale = ContentScale.Fit, ) if (isPrivate) { Box( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/EmojiPackViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/EmojiPackViewModel.kt index 54380e46b..c689ac7a0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/EmojiPackViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/EmojiPackViewModel.kt @@ -77,14 +77,6 @@ class EmojiPackViewModel( account.deleteOwnedEmojiPack(packIdentifier) } - /** - * Uploads an image selected from the gallery to the account's default file - * server (NIP-96 or Blossom) and calls [onUploaded] with the resulting URL. - * - * Mirrors the uploader pattern used by - * `BookmarkGroupMetadataViewModel.uploadForPicture` — see that file for the - * canonical implementation. - */ fun uploadEmojiImage( uri: SelectedMedia, context: Context, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/MyEmojiListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/MyEmojiListScreen.kt index f6d40e580..5716471cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/MyEmojiListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/MyEmojiListScreen.kt @@ -200,12 +200,17 @@ private fun SelectedEmojiPackCard( remember(packEvent) { packEvent?.taggedEmojis()?.map { it.url }.orEmpty() } + val author = + remember(packNote) { + if (accountViewModel.isLoggedUser(packNote.author)) null else packNote.author?.toBestDisplayName() + } Box(modifier = modifier.fillMaxWidth()) { EmojiPackCard( title = title, emojiUrls = emojiUrls, coverImage = image, + author = author, onClick = onOpen, ) // Remove button is on top-start so it doesn't clash with the cover badge (top-end). diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index d4a1b8699..e8eceb895 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -2398,6 +2398,7 @@ Add emoji Add custom emoji Remove :%1$s:? + Long-press an emoji to remove it \"%1$s\" is in your emoji list \"%1$s\" is not in your emoji list Emoji pack actions diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/EmojiPackState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/EmojiPackState.kt index 09629b762..2fc9bfbf3 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/EmojiPackState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/EmojiPackState.kt @@ -126,9 +126,9 @@ class EmojiPackState( suspend fun addEmojiPack(emojiPack: Note): EmojiPackSelectionEvent { val emojiPackEvent = emojiPack.event - if (emojiPackEvent !is EmojiPackEvent) throw IllegalArgumentException("Cannot add an emoji pack to this kind of event.") + if (emojiPackEvent !is EmojiPackEvent) throw IllegalArgumentException("Note is not an EmojiPackEvent; cannot add to emoji list.") - val eventHint = emojiPack.toEventHint() ?: throw IllegalArgumentException("Cannot add an emoji pack to this kind of event.") + val eventHint = emojiPack.toEventHint() ?: throw IllegalArgumentException("Cannot build event hint for this emoji pack.") val usersEmojiList = getEmojiPackSelection() return if (usersEmojiList == null) { @@ -141,7 +141,7 @@ class EmojiPackState( } suspend fun removeEmojiPack(emojiPack: Note): EmojiPackSelectionEvent? { - val usersEmojiList = getEmojiPackSelection() ?: throw IllegalArgumentException("Cannot remove an emoji pack to this kind of event.") + val usersEmojiList = getEmojiPackSelection() ?: throw IllegalArgumentException("No emoji pack selection exists to remove from.") val emojiPackEvent = emojiPack.event if (emojiPackEvent !is EmojiPackEvent) return null