diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index d17e2a349..3b65d75b5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -468,6 +468,12 @@ class Account( } } + suspend fun changeReactionRowItems(items: List) { + if (settings.changeReactionRowItems(items)) { + sendNewAppSpecificData() + } + } + suspend fun updateZapAmounts( amountSet: List, selectedZapType: LnZapEvent.ZapType, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index 8edbdad23..94c178cd6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -235,6 +235,15 @@ class AccountSettings( return false } + fun changeReactionRowItems(newItems: List): Boolean { + if (syncedSettings.reactions.reactionRowItems.value != newItems) { + syncedSettings.reactions.reactionRowItems.tryEmit(newItems.toImmutableList()) + saveAccountSettings() + return true + } + return false + } + fun changeZapPaymentRequest(newServer: Nip47WalletConnect.Nip47URINorm?): Boolean { if (zapPaymentRequest.value != newServer) { zapPaymentRequest.tryEmit(newServer) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt index a517b7c1a..eb5006401 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt @@ -33,7 +33,11 @@ import java.util.Locale class AccountSyncedSettings( internalSettings: AccountSyncedSettingsInternal, ) { - val reactions = AccountReactionPreferences(MutableStateFlow(internalSettings.reactions.reactionChoices.toImmutableList())) + val reactions = + AccountReactionPreferences( + MutableStateFlow(internalSettings.reactions.reactionChoices.toImmutableList()), + MutableStateFlow(internalSettings.reactions.reactionRowItems.toImmutableList()), + ) val zaps = AccountZapPreferences( MutableStateFlow(internalSettings.zaps.zapAmountChoices.toImmutableList()), @@ -54,7 +58,7 @@ class AccountSyncedSettings( fun toInternal(): AccountSyncedSettingsInternal = AccountSyncedSettingsInternal( - reactions = AccountReactionPreferencesInternal(reactions.reactionChoices.value), + reactions = AccountReactionPreferencesInternal(reactions.reactionChoices.value, reactions.reactionRowItems.value), zaps = AccountZapPreferencesInternal( zaps.zapAmountChoices.value, @@ -80,6 +84,11 @@ class AccountSyncedSettings( reactions.reactionChoices.tryEmit(newReactionChoices) } + val newReactionRowItems = syncedSettingsInternal.reactions.reactionRowItems.toImmutableList() + if (!equalImmutableLists(reactions.reactionRowItems.value, newReactionRowItems)) { + reactions.reactionRowItems.tryEmit(newReactionRowItems) + } + val newZapChoices = syncedSettingsInternal.zaps.zapAmountChoices.toImmutableList() if (!equalImmutableLists(zaps.zapAmountChoices.value, newZapChoices)) { zaps.zapAmountChoices.tryEmit(newZapChoices) @@ -120,6 +129,7 @@ class AccountSyncedSettings( @Stable class AccountReactionPreferences( var reactionChoices: MutableStateFlow>, + var reactionRowItems: MutableStateFlow>, ) @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt index 0ef0f5d4c..5dbb8727b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt @@ -39,6 +39,31 @@ val DefaultReactions = val DefaultZapAmounts = listOf(100L, 500L, 1000L) +@Serializable +enum class ReactionRowAction { + Reply, + Boost, + Like, + Zap, + Share, +} + +@Serializable +data class ReactionRowItem( + val action: ReactionRowAction, + val enabled: Boolean = true, + val showCounter: Boolean = true, +) + +val DefaultReactionRowItems = + listOf( + ReactionRowItem(ReactionRowAction.Reply), + ReactionRowItem(ReactionRowAction.Boost), + ReactionRowItem(ReactionRowAction.Like), + ReactionRowItem(ReactionRowAction.Zap), + ReactionRowItem(ReactionRowAction.Share, showCounter = false), + ) + fun getLanguagesSpokenByUser(): Set { val languageList = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration()) val codedList = mutableSetOf() @@ -59,6 +84,7 @@ class AccountSyncedSettingsInternal( @Serializable class AccountReactionPreferencesInternal( var reactionChoices: List = DefaultReactions, + var reactionRowItems: List = DefaultReactionRowItems, ) @Serializable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index df3db2891..f032e1825 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -109,6 +109,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationScre import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.AllSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NIP47SetupScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.ReactionsSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SecurityFiltersScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.UpdateZapAmountScreen @@ -175,6 +176,7 @@ fun AppNavigation( composableFromEnd { DraftListScreen(accountViewModel, nav) } composableFromEnd { SettingsScreen(accountViewModel, nav) } composableFromEnd { UserSettingsScreen(accountViewModel, nav) } + composableFromEnd { ReactionsSettingsScreen(accountViewModel, nav) } composableFromEndArgs { NIP47SetupScreen(accountViewModel, nav, it.nip47) } composableFromEndArgs { UpdateZapAmountScreen(accountViewModel, nav, it.nip47) } composableFromEndArgs { AllRelayListScreen(accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt index f5fa4a8fd..ef789d612 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt @@ -88,6 +88,8 @@ sealed class Route { @Serializable object UserSettings : Route() + @Serializable object ReactionsSettings : Route() + @Serializable object Lists : Route() @Serializable data class MyPeopleListView( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 2bb7af9fe..85dbd35e2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -100,6 +100,8 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.ReactionRowAction +import com.vitorpamplona.amethyst.model.ReactionRowItem import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReactionCount @@ -212,6 +214,7 @@ private fun InnerReactionRow( nav: INav, ) { val voiceRecordingState = remember(baseNote.idHex) { mutableStateOf(false) } + val reactionRowItems by accountViewModel.reactionRowItemsFlow().collectAsStateWithLifecycle() GenericInnerReactionRow( showReactionDetail = showReactionDetail, @@ -222,39 +225,62 @@ private fun InnerReactionRow( RenderShowIndividualReactionsButton(wantsToSeeReactions, accountViewModel) } }, - two = { - ReplyReactionWithDialog( - baseNote, - MaterialTheme.colorScheme.placeholderText, - accountViewModel, - nav, - voiceRecordingState = voiceRecordingState, - ) - }, - three = { - val isDM = baseNote.event is ChatroomKeyable - if (!isDM) { - BoostWithDialog( - baseNote, - editState, - MaterialTheme.colorScheme.placeholderText, - accountViewModel, - nav, - ) + reactions = reactionRowItems, + renderReaction = { item -> + when (item.action) { + ReactionRowAction.Reply -> { + ReplyReactionWithDialog( + baseNote, + MaterialTheme.colorScheme.placeholderText, + accountViewModel, + nav, + showCounter = item.showCounter, + voiceRecordingState = voiceRecordingState, + ) + } + + ReactionRowAction.Boost -> { + val isDM = baseNote.event is ChatroomKeyable + if (!isDM) { + BoostWithDialog( + baseNote, + editState, + MaterialTheme.colorScheme.placeholderText, + accountViewModel, + nav, + showCounter = item.showCounter, + ) + } + } + + ReactionRowAction.Like -> { + LikeReaction( + baseNote, + MaterialTheme.colorScheme.placeholderText, + accountViewModel, + nav, + showCounter = item.showCounter, + ) + } + + ReactionRowAction.Zap -> { + ZapReaction( + baseNote, + MaterialTheme.colorScheme.placeholderText, + accountViewModel, + nav = nav, + showCounter = item.showCounter, + ) + } + + ReactionRowAction.Share -> { + ShareReaction( + note = baseNote, + grayTint = MaterialTheme.colorScheme.placeholderText, + ) + } } }, - four = { - LikeReaction(baseNote, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav) - }, - five = { - ZapReaction(baseNote, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav = nav) - }, - six = { - ShareReaction( - note = baseNote, - grayTint = MaterialTheme.colorScheme.placeholderText, - ) - }, ) } @@ -301,12 +327,12 @@ private fun GenericInnerReactionRow( addPadding: Boolean, weightTwo: Float = 1f, one: @Composable () -> Unit, - two: @Composable () -> Unit, - three: @Composable () -> Unit, - four: @Composable () -> Unit, - five: @Composable () -> Unit, - six: @Composable () -> Unit, + reactions: ImmutableList, + renderReaction: @Composable (ReactionRowItem) -> Unit, ) { + val enabledReactions = remember(reactions) { reactions.filter { it.enabled } } + val lastIndex = enabledReactions.lastIndex + Row( verticalAlignment = CenterVertically, horizontalArrangement = RowColSpacing, @@ -321,15 +347,18 @@ private fun GenericInnerReactionRow( } } - Row(verticalAlignment = CenterVertically, horizontalArrangement = RowColSpacing, modifier = Modifier.weight(weightTwo)) { two() } - - Row(verticalAlignment = CenterVertically, horizontalArrangement = RowColSpacing, modifier = Modifier.weight(1f)) { three() } - - Row(verticalAlignment = CenterVertically, horizontalArrangement = RowColSpacing, modifier = Modifier.weight(1f)) { four() } - - Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) { five() } - - Row(verticalAlignment = CenterVertically, modifier = Modifier) { six() } + enabledReactions.forEachIndexed { index, item -> + val isLast = index == lastIndex + val itemWeight = if (item.action == ReactionRowAction.Reply) weightTwo else 1f + val mod = if (isLast) Modifier else Modifier.weight(itemWeight) + Row( + verticalAlignment = CenterVertically, + horizontalArrangement = RowColSpacing, + modifier = mod, + ) { + renderReaction(item) + } + } } } @@ -548,11 +577,13 @@ private fun BoostWithDialog( grayTint: Color, accountViewModel: AccountViewModel, nav: INav, + showCounter: Boolean = true, ) { BoostReaction( baseNote, grayTint, accountViewModel, + showCounter = showCounter, onQuotePress = { nav.nav { Route.NewShortNote( @@ -597,6 +628,7 @@ private fun ReplyReactionWithDialog( grayTint: Color, accountViewModel: AccountViewModel, nav: INav, + showCounter: Boolean = true, voiceRecordingState: MutableState? = null, ) { if (baseNote.event is BaseVoiceEvent) { @@ -605,10 +637,11 @@ private fun ReplyReactionWithDialog( grayTint, accountViewModel, nav, + showCounter = showCounter, voiceRecordingState = voiceRecordingState, ) } else { - ReplyReaction(baseNote, grayTint, accountViewModel) { + ReplyReaction(baseNote, grayTint, accountViewModel, showCounter = showCounter) { nav.nav { routeReplyTo(baseNote, accountViewModel.account) } } } @@ -800,6 +833,7 @@ fun BoostReaction( accountViewModel: AccountViewModel, iconSizeModifier: Modifier = Size19Modifier, iconSize: Dp = Size20dp, + showCounter: Boolean = true, onQuotePress: () -> Unit, onForkPress: () -> Unit, ) { @@ -836,7 +870,9 @@ fun BoostReaction( } } - BoostText(baseNote, grayTint, accountViewModel) + if (showCounter) { + BoostText(baseNote, grayTint, accountViewModel) + } } @Composable @@ -870,6 +906,7 @@ fun LikeReaction( iconSize: Dp = Size18dp, heartSizeModifier: Modifier = Size18Modifier, iconFontSize: TextUnit = Font14SP, + showCounter: Boolean = true, ) { var wantsToReact by remember { mutableStateOf(false) } @@ -908,7 +945,9 @@ fun LikeReaction( } } - ObserveLikeText(baseNote, accountViewModel) { reactionCount -> SlidingAnimationCount(reactionCount, grayTint, accountViewModel) } + if (showCounter) { + ObserveLikeText(baseNote, accountViewModel) { reactionCount -> SlidingAnimationCount(reactionCount, grayTint, accountViewModel) } + } } @Composable @@ -1027,6 +1066,7 @@ fun ZapReaction( iconSize: Dp = Size20dp, iconSizeModifier: Modifier = Size20Modifier, animationModifier: Modifier = Size14Modifier, + showCounter: Boolean = true, nav: INav, ) { var wantsToZap by remember { mutableStateOf(false) } @@ -1194,8 +1234,10 @@ fun ZapReaction( } } - ObserveZapAmountText(baseNote, accountViewModel) { zapAmountTxt -> - SlidingAnimationAmount(zapAmountTxt, grayTint, accountViewModel) + if (showCounter) { + ObserveZapAmountText(baseNote, accountViewModel) { zapAmountTxt -> + SlidingAnimationAmount(zapAmountTxt, grayTint, accountViewModel) + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 4bf0127d4..1e0cea279 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -986,6 +986,13 @@ class AccountViewModel( onDone() } + fun reactionRowItemsFlow() = account.settings.syncedSettings.reactions.reactionRowItems + + fun changeReactionRowItems(items: List) = + launchSigner { + account.changeReactionRowItems(items) + } + fun updateZapAmounts( amountSet: List, selectedZapType: LnZapEvent.ZapType, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt index 2157afe8a..7d2bcf860 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt @@ -33,6 +33,7 @@ import androidx.compose.material.icons.outlined.FavoriteBorder import androidx.compose.material.icons.outlined.Key import androidx.compose.material.icons.outlined.Security import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material.icons.outlined.ThumbUp import androidx.compose.material.icons.outlined.Translate import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -153,6 +154,13 @@ fun AllSettingsScreen( tint = tint, onClick = { nav.nav(Route.Settings) }, ) + HorizontalDivider() + SettingsNavigationRow( + title = R.string.reactions_settings, + icon = Icons.Outlined.ThumbUp, + tint = tint, + onClick = { nav.nav(Route.ReactionsSettings) }, + ) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ReactionsSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ReactionsSettingsScreen.kt new file mode 100644 index 000000000..3a663cc68 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ReactionsSettingsScreen.kt @@ -0,0 +1,280 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings + +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.ReactionRowAction +import com.vitorpamplona.amethyst.model.ReactionRowItem +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size20dp + +@Composable +fun ReactionsSettingsScreen( + accountViewModel: AccountViewModel, + nav: INav, +) { + Scaffold( + topBar = { + TopBarWithBackButton(stringRes(id = R.string.reactions_settings), nav::popBack) + }, + ) { padding -> + Column(Modifier.padding(padding)) { + ReactionsSettingsContent(accountViewModel) + } + } +} + +@Composable +fun ReactionsSettingsContent(accountViewModel: AccountViewModel) { + val reactionRowItems by accountViewModel.reactionRowItemsFlow().collectAsStateWithLifecycle() + var items by remember(reactionRowItems) { mutableStateOf(reactionRowItems.toMutableList()) } + + fun save(newItems: List) { + items = newItems.toMutableList() + accountViewModel.changeReactionRowItems(newItems) + } + + Column( + modifier = + Modifier + .fillMaxSize() + .padding(horizontal = Size20dp) + .verticalScroll(rememberScrollState()), + ) { + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringRes(R.string.reactions_settings_description), + style = MaterialTheme.typography.bodyMedium, + color = Color.Gray, + modifier = Modifier.padding(bottom = 16.dp), + ) + + items.forEachIndexed { index, item -> + ReactionRowItemCard( + item = item, + canMoveUp = index > 0, + canMoveDown = index < items.lastIndex, + onToggleEnabled = { + val newItems = items.toMutableList() + newItems[index] = item.copy(enabled = !item.enabled) + save(newItems) + }, + onToggleCounter = { + val newItems = items.toMutableList() + newItems[index] = item.copy(showCounter = !item.showCounter) + save(newItems) + }, + onMoveUp = { + if (index > 0) { + val newItems = items.toMutableList() + val temp = newItems[index - 1] + newItems[index - 1] = newItems[index] + newItems[index] = temp + save(newItems) + } + }, + onMoveDown = { + if (index < items.lastIndex) { + val newItems = items.toMutableList() + val temp = newItems[index + 1] + newItems[index + 1] = newItems[index] + newItems[index] = temp + save(newItems) + } + }, + ) + if (index < items.lastIndex) { + HorizontalDivider() + } + } + + Spacer(modifier = Modifier.height(16.dp)) + } +} + +@Composable +private fun ReactionRowItemCard( + item: ReactionRowItem, + canMoveUp: Boolean, + canMoveDown: Boolean, + onToggleEnabled: () -> Unit, + onToggleCounter: () -> Unit, + onMoveUp: () -> Unit, + onMoveDown: () -> Unit, +) { + val actionName = reactionActionName(item.action) + val actionDescription = reactionActionDescription(item.action) + + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = actionName, + style = MaterialTheme.typography.bodyLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = actionDescription, + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + IconButton( + onClick = onMoveUp, + enabled = canMoveUp, + modifier = Modifier.size(32.dp), + ) { + Icon( + Icons.Default.KeyboardArrowUp, + contentDescription = stringRes(R.string.reactions_settings_move_up), + modifier = Modifier.size(20.dp), + tint = if (canMoveUp) MaterialTheme.colorScheme.onSurface else Color.Gray, + ) + } + IconButton( + onClick = onMoveDown, + enabled = canMoveDown, + modifier = Modifier.size(32.dp), + ) { + Icon( + Icons.Default.KeyboardArrowDown, + contentDescription = stringRes(R.string.reactions_settings_move_down), + modifier = Modifier.size(20.dp), + tint = if (canMoveDown) MaterialTheme.colorScheme.onSurface else Color.Gray, + ) + } + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + Row( + modifier = Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Switch( + checked = item.enabled, + onCheckedChange = { onToggleEnabled() }, + ) + Text( + text = stringRes(R.string.reactions_settings_enabled), + style = MaterialTheme.typography.bodyMedium, + ) + } + + if (item.action != ReactionRowAction.Share) { + Row( + modifier = Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Switch( + checked = item.showCounter, + onCheckedChange = { onToggleCounter() }, + enabled = item.enabled, + ) + Text( + text = stringRes(R.string.reactions_settings_show_counter), + style = MaterialTheme.typography.bodyMedium, + color = if (item.enabled) MaterialTheme.colorScheme.onSurface else Color.Gray, + ) + } + } else { + Spacer(modifier = Modifier.weight(1f)) + } + } + } +} + +@Composable +fun reactionActionName(action: ReactionRowAction): String = + when (action) { + ReactionRowAction.Reply -> stringRes(R.string.reactions_settings_reply) + ReactionRowAction.Boost -> stringRes(R.string.reactions_settings_boost) + ReactionRowAction.Like -> stringRes(R.string.reactions_settings_like) + ReactionRowAction.Zap -> stringRes(R.string.reactions_settings_zap) + ReactionRowAction.Share -> stringRes(R.string.reactions_settings_share) + } + +@Composable +fun reactionActionDescription(action: ReactionRowAction): String = + when (action) { + ReactionRowAction.Reply -> stringRes(R.string.reactions_settings_reply_description) + ReactionRowAction.Boost -> stringRes(R.string.reactions_settings_boost_description) + ReactionRowAction.Like -> stringRes(R.string.reactions_settings_like_description) + ReactionRowAction.Zap -> stringRes(R.string.reactions_settings_zap_description) + ReactionRowAction.Share -> stringRes(R.string.reactions_settings_share_description) + } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 76146bd36..99ff0cc66 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1234,6 +1234,23 @@ Change Quick Reactions + Reactions Settings + Configure which reaction buttons are shown, their order, and whether to display counters. + Enabled + Show Count + Move up + Move down + Reply + Reply to this note + Boost + Repost or quote this note + Like + React to this note with an emoji + Zap + Send a lightning payment to the author + Share + Share this note externally + Profile Picture of %1$s Relay %1$s Expand relay list