From 1b8b508816000bfa2dd6ff436ed8e754072d1752 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 13 May 2026 19:04:33 +0000 Subject: [PATCH 1/3] feat: add Compose Settings screen with auto-draft toggle Introduces a new Compose Settings screen that groups composer-related preferences. Adds a new "Automatically create drafts" toggle that gates the automatic draft creation triggered on back-press / cancel across every composer (short notes, public messages, long-form, classifieds, public channels, private DMs, nests). Moves "Propose text improvements" and "Tracked broadcasts" out of Application Preferences and into the new Compose Settings screen. https://claude.ai/code/session_019b6cF7Ukkv7GL9A3m6bXym --- .../amethyst/model/UiSettings.kt | 1 + .../amethyst/model/UiSettingsFlow.kt | 13 ++- .../model/preferences/UISharedPreferences.kt | 3 + .../amethyst/ui/navigation/AppNavigation.kt | 2 + .../amethyst/ui/navigation/routes/Routes.kt | 2 + .../amethyst/ui/screen/UiSettingsState.kt | 2 + .../privateDM/send/ChatNewMessageViewModel.kt | 1 + .../send/ChannelNewMessageViewModel.kt | 1 + .../nip23LongForm/LongFormPostViewModel.kt | 1 + .../nip99Classifieds/NewProductViewModel.kt | 1 + .../loggedIn/home/ShortNotePostViewModel.kt | 1 + .../room/chat/NestNewMessageViewModel.kt | 1 + .../NewPublicMessageViewModel.kt | 1 + .../loggedIn/settings/AllSettingsScreen.kt | 6 + .../loggedIn/settings/AppSettingsScreen.kt | 2 - .../settings/ComposeSettingsScreen.kt | 108 ++++++++++++++++++ amethyst/src/main/res/values/strings.xml | 3 + 17 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt index e1ebf5fa8c..17f756030d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt @@ -43,6 +43,7 @@ data class UiSettings( val gallerySet: ProfileGalleryType = ProfileGalleryType.CLASSIC, val automaticallyProposeAiImprovements: BooleanType = BooleanType.ALWAYS, val useTrackedBroadcasts: BooleanType = BooleanType.ALWAYS, + val automaticallyCreateDrafts: BooleanType = BooleanType.ALWAYS, val bottomBarItems: List = DefaultBottomBarItems, val showHomeNewThreadsTab: Boolean = true, val showHomeConversationsTab: Boolean = true, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt index 8a57956362..a6a00acbb3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt @@ -43,6 +43,7 @@ class UiSettingsFlow( val gallerySet: MutableStateFlow = MutableStateFlow(ProfileGalleryType.CLASSIC), val automaticallyProposeAiImprovements: MutableStateFlow = MutableStateFlow(BooleanType.ALWAYS), val useTrackedBroadcasts: MutableStateFlow = MutableStateFlow(BooleanType.ALWAYS), + val automaticallyCreateDrafts: MutableStateFlow = MutableStateFlow(BooleanType.ALWAYS), val bottomBarItems: MutableStateFlow> = MutableStateFlow(DefaultBottomBarItems), val showHomeNewThreadsTab: MutableStateFlow = MutableStateFlow(true), val showHomeConversationsTab: MutableStateFlow = MutableStateFlow(true), @@ -64,6 +65,7 @@ class UiSettingsFlow( gallerySet, automaticallyProposeAiImprovements, useTrackedBroadcasts, + automaticallyCreateDrafts, bottomBarItems, showHomeNewThreadsTab, showHomeConversationsTab, @@ -89,10 +91,11 @@ class UiSettingsFlow( flows[11] as ProfileGalleryType, flows[12] as BooleanType, flows[13] as BooleanType, - flows[14] as List, - flows[15] as Boolean, + flows[14] as BooleanType, + flows[15] as List, flows[16] as Boolean, flows[17] as Boolean, + flows[18] as Boolean, ) } @@ -112,6 +115,7 @@ class UiSettingsFlow( gallerySet.value, automaticallyProposeAiImprovements.value, useTrackedBroadcasts.value, + automaticallyCreateDrafts.value, bottomBarItems.value, showHomeNewThreadsTab.value, showHomeConversationsTab.value, @@ -177,6 +181,10 @@ class UiSettingsFlow( useTrackedBroadcasts.tryEmit(torSettings.useTrackedBroadcasts) any = true } + if (automaticallyCreateDrafts.value != torSettings.automaticallyCreateDrafts) { + automaticallyCreateDrafts.tryEmit(torSettings.automaticallyCreateDrafts) + any = true + } if (bottomBarItems.value != torSettings.bottomBarItems) { bottomBarItems.tryEmit(torSettings.bottomBarItems) any = true @@ -226,6 +234,7 @@ class UiSettingsFlow( MutableStateFlow(uiSettings.gallerySet), MutableStateFlow(uiSettings.automaticallyProposeAiImprovements), MutableStateFlow(uiSettings.useTrackedBroadcasts), + MutableStateFlow(uiSettings.automaticallyCreateDrafts), MutableStateFlow(uiSettings.bottomBarItems), MutableStateFlow(uiSettings.showHomeNewThreadsTab), MutableStateFlow(uiSettings.showHomeConversationsTab), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt index c1eefef1e5..36dbe92863 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt @@ -107,6 +107,7 @@ class UiSharedPreferences( val UI_GALLERY_SET = stringPreferencesKey("ui.gallery_set") val UI_PROPOSE_AI_IMPROVEMENTS = stringPreferencesKey("ui.propose_ai_improvements") val UI_USE_TRACKED_BROADCASTS = stringPreferencesKey("ui.use_tracked_broadcasts") + val UI_AUTOMATICALLY_CREATE_DRAFTS = stringPreferencesKey("ui.automatically_create_drafts") val UI_BOTTOM_BAR_ITEMS = stringPreferencesKey("ui.bottom_bar_items") val UI_SHOW_HOME_NEW_THREADS_TAB = booleanPreferencesKey("ui.show_home_new_threads_tab") val UI_SHOW_HOME_CONVERSATIONS_TAB = booleanPreferencesKey("ui.show_home_conversations_tab") @@ -136,6 +137,7 @@ class UiSharedPreferences( useTrackedBroadcasts = preferences[UI_USE_TRACKED_BROADCASTS]?.let { BooleanType.valueOf(it) } ?: if (featureSet == FeatureSetType.COMPLETE) BooleanType.ALWAYS else BooleanType.NEVER, + automaticallyCreateDrafts = preferences[UI_AUTOMATICALLY_CREATE_DRAFTS]?.let { BooleanType.valueOf(it) } ?: BooleanType.ALWAYS, bottomBarItems = preferences[UI_BOTTOM_BAR_ITEMS]?.let { decodeBottomBarItems(it) } ?: DefaultBottomBarItems, showHomeNewThreadsTab = preferences[UI_SHOW_HOME_NEW_THREADS_TAB] ?: true, showHomeConversationsTab = preferences[UI_SHOW_HOME_CONVERSATIONS_TAB] ?: true, @@ -178,6 +180,7 @@ class UiSharedPreferences( preferences[UI_GALLERY_SET] = sharedSettings.gallerySet.name preferences[UI_PROPOSE_AI_IMPROVEMENTS] = sharedSettings.automaticallyProposeAiImprovements.name preferences[UI_USE_TRACKED_BROADCASTS] = sharedSettings.useTrackedBroadcasts.name + preferences[UI_AUTOMATICALLY_CREATE_DRAFTS] = sharedSettings.automaticallyCreateDrafts.name preferences[UI_BOTTOM_BAR_ITEMS] = sharedSettings.bottomBarItems.joinToString(",") { it.name } preferences[UI_SHOW_HOME_NEW_THREADS_TAB] = sharedSettings.showHomeNewThreadsTab preferences[UI_SHOW_HOME_CONVERSATIONS_TAB] = sharedSettings.showHomeConversationsTab 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 e0beff8e37..44e61bcfdd 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 @@ -156,6 +156,7 @@ 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.BottomBarSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.CallSettingsScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.ComposeSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.HomeTabsSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NIP47SetupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NamecoinSettingsScreen @@ -307,6 +308,7 @@ fun BuildNavigation( composableFromEnd { DraftListScreen(accountViewModel, nav) } composableFromEnd { ScheduledPostsScreen(accountViewModel, nav) } composableFromEnd { SettingsScreen(accountViewModel, nav) } + composableFromEnd { ComposeSettingsScreen(accountViewModel, nav) } composableFromEnd { UserSettingsScreen(accountViewModel, nav) } composableFromEnd { ReactionsSettingsScreen(accountViewModel, nav) } composableFromEnd { BottomBarSettingsScreen(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 8c1ac58dcb..7607abd2f6 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 @@ -215,6 +215,8 @@ sealed class Route { @Serializable object Settings : Route() + @Serializable object ComposeSettings : Route() + @Serializable object UserSettings : Route() @Serializable object ReactionsSettings : Route() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UiSettingsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UiSettingsState.kt index bd28f91bf7..d0a3e1c387 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UiSettingsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UiSettingsState.kt @@ -133,6 +133,8 @@ class UiSettingsState( fun useTrackedBroadcasts() = uiSettingsFlow.useTrackedBroadcasts.value == BooleanType.ALWAYS + fun automaticallyCreateDrafts() = uiSettingsFlow.automaticallyCreateDrafts.value == BooleanType.ALWAYS + fun isImmersiveScrollingActive() = uiSettingsFlow.automaticallyHideNavigationBars.value == BooleanType.ALWAYS fun showProfilePictures() = showProfilePictures.value 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 17e74d75a3..40d9f3d70f 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 @@ -422,6 +422,7 @@ class ChatNewMessageViewModel : } suspend fun sendDraftSync() { + if (!accountViewModel.settings.automaticallyCreateDrafts()) return if (message.text.toString().isBlank()) { account.deleteDraftIgnoreErrors(draftTag.current) } else { 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 805f6f5a99..c4d1ecf9db 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 @@ -304,6 +304,7 @@ open class ChannelNewMessageViewModel : } suspend fun sendDraftSync() { + if (!accountViewModel.settings.automaticallyCreateDrafts()) return if (message.text.toString().isBlank()) { account.deleteDraftIgnoreErrors(draftTag.current) } else { 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 972ed92cc5..1cc9d73365 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 @@ -346,6 +346,7 @@ class LongFormPostViewModel : } suspend fun sendDraftSync() { + if (!accountViewModel.settings.automaticallyCreateDrafts()) return if (message.text.toString().isBlank() && title.text.isBlank()) { accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) } else { 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 608fdc5cf3..99fc4a4378 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 @@ -325,6 +325,7 @@ open class NewProductViewModel : } suspend fun sendDraftSync() { + if (!accountViewModel.settings.automaticallyCreateDrafts()) return if (message.text.toString().isBlank()) { accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) } else { 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 5339a4a253..3320726e9c 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 @@ -895,6 +895,7 @@ open class ShortNotePostViewModel : } suspend fun sendDraftSync() { + if (!accountViewModel.settings.automaticallyCreateDrafts()) return if (message.text.toString().isBlank()) { accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) } else { 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 a2ba9be4d9..7ba63692e5 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 @@ -308,6 +308,7 @@ open class NestNewMessageViewModel : } suspend fun sendDraftSync() { + if (!accountViewModel.settings.automaticallyCreateDrafts()) return if (message.text.toString().isBlank()) { account.deleteDraftIgnoreErrors(draftTag.current) } else { 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 25b5fd7330..cd9f0029a2 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 @@ -350,6 +350,7 @@ class NewPublicMessageViewModel : } suspend fun sendDraftSync() { + if (!accountViewModel.settings.automaticallyCreateDrafts()) return if (message.text.toString().isBlank()) { accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) } else { 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 a6cec1a382..a24133ff43 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 @@ -236,6 +236,12 @@ fun AllSettingsScreen( onClick = { nav.nav(Route.Settings) }, ) SettingsDivider() + SettingsItem( + title = R.string.compose_settings, + icon = MaterialSymbols.Edit, + onClick = { nav.nav(Route.ComposeSettings) }, + ) + SettingsDivider() SettingsItem( title = R.string.reactions_settings, icon = MaterialSymbols.ThumbUp, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt index f31857f149..512c5676be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt @@ -136,8 +136,6 @@ fun SettingsScreen( ImmersiveScrollingChoice(sharedPrefs) FeatureSetChoice(sharedPrefs) GalleryChoice(sharedPrefs) - AiWritingHelpChoice(sharedPrefs) - TrackedBroadcastsChoice(sharedPrefs) PushNotificationSettingsRow(sharedPrefs) if (accountViewModel != null) { AlwaysOnNotificationServiceChoice(accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt new file mode 100644 index 0000000000..074ef0798e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt @@ -0,0 +1,108 @@ +/* + * 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.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.BooleanType +import com.vitorpamplona.amethyst.model.UiSettingsFlow +import com.vitorpamplona.amethyst.model.parseBooleanType +import com.vitorpamplona.amethyst.ui.components.TitleExplainer +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.RowColSpacing +import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.Size20dp +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun ComposeSettingsScreen( + accountViewModel: AccountViewModel, + nav: INav, +) { + Scaffold( + topBar = { + TopBarWithBackButton(stringRes(id = R.string.compose_settings), nav) + }, + ) { + Column(Modifier.padding(it)) { + ComposeSettingsContent(accountViewModel.settings.uiSettingsFlow) + } + } +} + +@Preview(device = "spec:width=2160px,height=2340px,dpi=440") +@Composable +fun ComposeSettingsScreenPreview() { + ThemeComparisonRow { + ComposeSettingsContent(UiSettingsFlow()) + } +} + +@Composable +fun ComposeSettingsContent(sharedPrefs: UiSettingsFlow) { + Column( + Modifier + .fillMaxSize() + .padding(top = Size10dp, start = Size20dp, end = Size20dp) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = RowColSpacing, + ) { + AutoCreateDraftsChoice(sharedPrefs) + AiWritingHelpChoice(sharedPrefs) + TrackedBroadcastsChoice(sharedPrefs) + } +} + +@Composable +fun AutoCreateDraftsChoice(sharedPrefs: UiSettingsFlow) { + val createDraftsIndex by sharedPrefs.automaticallyCreateDrafts.collectAsState() + + val booleanItems = + persistentListOf( + TitleExplainer(stringRes(BooleanType.ALWAYS.reourceId)), + TitleExplainer(stringRes(BooleanType.NEVER.reourceId)), + ) + + SettingsRow( + R.string.auto_create_drafts_setting_title, + R.string.auto_create_drafts_setting_description, + booleanItems, + createDraftsIndex.screenCode, + ) { + sharedPrefs.automaticallyCreateDrafts.tryEmit(parseBooleanType(it)) + } +} diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 4ec6447448..b31283c26d 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -2794,6 +2794,9 @@ Uses an on-device AI model to propose text corrections and tone changes. Tracked broadcasts Use the tracked broadcaster when sending events. Shows live progress and per-relay status while broadcasting. + Compose Settings + Automatically create drafts + Saves a draft event automatically when you leave a composer with unsent text. Disable to keep your unfinished posts local until you publish. Use This Dismiss Correct From d5860841c91c893737de6e1db7f1f7f6612d1084 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 13 May 2026 19:28:27 +0000 Subject: [PATCH 2/3] refactor: gate only draft create, and colocate compose composables - sendDraftSync no longer short-circuits the delete-on-blank path when automatic draft creation is disabled. Clearing a composer now always cleans up any existing draft; the setting only suppresses creation. - Physically move AiWritingHelpChoice and TrackedBroadcastsChoice from AppSettingsScreen.kt into ComposeSettingsScreen.kt so the composable definitions live alongside the screen that hosts them. https://claude.ai/code/session_019b6cF7Ukkv7GL9A3m6bXym --- .../privateDM/send/ChatNewMessageViewModel.kt | 3 +- .../send/ChannelNewMessageViewModel.kt | 3 +- .../nip23LongForm/LongFormPostViewModel.kt | 3 +- .../nip99Classifieds/NewProductViewModel.kt | 3 +- .../loggedIn/home/ShortNotePostViewModel.kt | 3 +- .../room/chat/NestNewMessageViewModel.kt | 3 +- .../NewPublicMessageViewModel.kt | 3 +- .../loggedIn/settings/AppSettingsScreen.kt | 41 ------------------- .../settings/ComposeSettingsScreen.kt | 41 +++++++++++++++++++ 9 files changed, 48 insertions(+), 55 deletions(-) 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 40d9f3d70f..18181978ea 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 @@ -422,10 +422,9 @@ class ChatNewMessageViewModel : } suspend fun sendDraftSync() { - if (!accountViewModel.settings.automaticallyCreateDrafts()) return if (message.text.toString().isBlank()) { account.deleteDraftIgnoreErrors(draftTag.current) - } else { + } else if (accountViewModel.settings.automaticallyCreateDrafts()) { innerSendPost(draftTag.current) } } 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 c4d1ecf9db..1380c965b4 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 @@ -304,10 +304,9 @@ open class ChannelNewMessageViewModel : } suspend fun sendDraftSync() { - if (!accountViewModel.settings.automaticallyCreateDrafts()) return if (message.text.toString().isBlank()) { account.deleteDraftIgnoreErrors(draftTag.current) - } else { + } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val attachments = mutableSetOf() nip95attachments.forEach { attachments.add(it.first) 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 1cc9d73365..cc508bf0d7 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 @@ -346,10 +346,9 @@ class LongFormPostViewModel : } suspend fun sendDraftSync() { - if (!accountViewModel.settings.automaticallyCreateDrafts()) return if (message.text.toString().isBlank() && title.text.isBlank()) { accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) - } else { + } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val template = createTemplate() ?: return accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, emptySet()) } 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 99fc4a4378..14d7a13347 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 @@ -325,10 +325,9 @@ open class NewProductViewModel : } suspend fun sendDraftSync() { - if (!accountViewModel.settings.automaticallyCreateDrafts()) return if (message.text.toString().isBlank()) { accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) - } else { + } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val template = createTemplate() ?: return accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template) } 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 3320726e9c..1504473697 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 @@ -895,10 +895,9 @@ open class ShortNotePostViewModel : } suspend fun sendDraftSync() { - if (!accountViewModel.settings.automaticallyCreateDrafts()) return if (message.text.toString().isBlank()) { accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) - } else { + } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val attachments = mutableSetOf() nip95attachments.forEach { attachments.add(it.first) 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 7ba63692e5..648a369000 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 @@ -308,10 +308,9 @@ open class NestNewMessageViewModel : } suspend fun sendDraftSync() { - if (!accountViewModel.settings.automaticallyCreateDrafts()) return if (message.text.toString().isBlank()) { account.deleteDraftIgnoreErrors(draftTag.current) - } else { + } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val attachments = mutableSetOf() nip95attachments.forEach { attachments.add(it.first) 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 cd9f0029a2..43d8ac6ca6 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 @@ -350,10 +350,9 @@ class NewPublicMessageViewModel : } suspend fun sendDraftSync() { - if (!accountViewModel.settings.automaticallyCreateDrafts()) return if (message.text.toString().isBlank()) { accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) - } else { + } else if (accountViewModel.settings.automaticallyCreateDrafts()) { val broadcast = mutableSetOf() nip95attachments.forEach { broadcast.add(it.first) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt index 512c5676be..9be0e54f6b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt @@ -55,7 +55,6 @@ import androidx.core.os.LocaleListCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.BooleanType import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.ProfileGalleryType @@ -403,46 +402,6 @@ fun GalleryChoice(sharedPrefs: UiSettingsFlow) { } } -@Composable -fun AiWritingHelpChoice(sharedPrefs: UiSettingsFlow) { - val aiIndex by sharedPrefs.automaticallyProposeAiImprovements.collectAsState() - - val booleanItems = - persistentListOf( - TitleExplainer(stringRes(ConnectivityType.ALWAYS.resourceId)), - TitleExplainer(stringRes(ConnectivityType.NEVER.resourceId)), - ) - - SettingsRow( - R.string.ai_writing_setting_title, - R.string.ai_writing_setting_description, - booleanItems, - aiIndex.screenCode, - ) { - sharedPrefs.automaticallyProposeAiImprovements.tryEmit(parseBooleanType(it)) - } -} - -@Composable -fun TrackedBroadcastsChoice(sharedPrefs: UiSettingsFlow) { - val useTrackedBroadcastsIndex by sharedPrefs.useTrackedBroadcasts.collectAsState() - - val booleanItems = - persistentListOf( - TitleExplainer(stringRes(BooleanType.ALWAYS.reourceId)), - TitleExplainer(stringRes(BooleanType.NEVER.reourceId)), - ) - - SettingsRow( - R.string.tracked_broadcasts_setting_title, - R.string.tracked_broadcasts_setting_description, - booleanItems, - useTrackedBroadcastsIndex.screenCode, - ) { - sharedPrefs.useTrackedBroadcasts.tryEmit(parseBooleanType(it)) - } -} - @Composable fun SettingsRow( name: Int, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt index 074ef0798e..1184c52cad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.BooleanType +import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.model.parseBooleanType import com.vitorpamplona.amethyst.ui.components.TitleExplainer @@ -106,3 +107,43 @@ fun AutoCreateDraftsChoice(sharedPrefs: UiSettingsFlow) { sharedPrefs.automaticallyCreateDrafts.tryEmit(parseBooleanType(it)) } } + +@Composable +fun AiWritingHelpChoice(sharedPrefs: UiSettingsFlow) { + val aiIndex by sharedPrefs.automaticallyProposeAiImprovements.collectAsState() + + val booleanItems = + persistentListOf( + TitleExplainer(stringRes(ConnectivityType.ALWAYS.resourceId)), + TitleExplainer(stringRes(ConnectivityType.NEVER.resourceId)), + ) + + SettingsRow( + R.string.ai_writing_setting_title, + R.string.ai_writing_setting_description, + booleanItems, + aiIndex.screenCode, + ) { + sharedPrefs.automaticallyProposeAiImprovements.tryEmit(parseBooleanType(it)) + } +} + +@Composable +fun TrackedBroadcastsChoice(sharedPrefs: UiSettingsFlow) { + val useTrackedBroadcastsIndex by sharedPrefs.useTrackedBroadcasts.collectAsState() + + val booleanItems = + persistentListOf( + TitleExplainer(stringRes(BooleanType.ALWAYS.reourceId)), + TitleExplainer(stringRes(BooleanType.NEVER.reourceId)), + ) + + SettingsRow( + R.string.tracked_broadcasts_setting_title, + R.string.tracked_broadcasts_setting_description, + booleanItems, + useTrackedBroadcastsIndex.screenCode, + ) { + sharedPrefs.useTrackedBroadcasts.tryEmit(parseBooleanType(it)) + } +} From 66202e667d192a94240b4ae8d87d347f96f78d2e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 13 May 2026 19:36:53 +0000 Subject: [PATCH 3/3] refactor: switch Compose Settings toggles to Material Switch Each binary setting (auto-create drafts, AI writing help, tracked broadcasts) becomes a single-tap Switch instead of a two-tap Always/Never spinner, and a shared BooleanSwitchRow helper removes the per-toggle boilerplate. https://claude.ai/code/session_019b6cF7Ukkv7GL9A3m6bXym --- .../settings/ComposeSettingsScreen.kt | 90 ++++++------------- 1 file changed, 29 insertions(+), 61 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt index 1184c52cad..1c5baba82f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -34,10 +35,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.BooleanType -import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.UiSettingsFlow -import com.vitorpamplona.amethyst.model.parseBooleanType -import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -46,7 +44,7 @@ import com.vitorpamplona.amethyst.ui.theme.RowColSpacing import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size20dp import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow -import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow @Composable fun ComposeSettingsScreen( @@ -82,68 +80,38 @@ fun ComposeSettingsContent(sharedPrefs: UiSettingsFlow) { horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = RowColSpacing, ) { - AutoCreateDraftsChoice(sharedPrefs) - AiWritingHelpChoice(sharedPrefs) - TrackedBroadcastsChoice(sharedPrefs) + BooleanSwitchRow( + sharedPrefs.automaticallyCreateDrafts, + R.string.auto_create_drafts_setting_title, + R.string.auto_create_drafts_setting_description, + ) + BooleanSwitchRow( + sharedPrefs.automaticallyProposeAiImprovements, + R.string.ai_writing_setting_title, + R.string.ai_writing_setting_description, + ) + BooleanSwitchRow( + sharedPrefs.useTrackedBroadcasts, + R.string.tracked_broadcasts_setting_title, + R.string.tracked_broadcasts_setting_description, + ) } } @Composable -fun AutoCreateDraftsChoice(sharedPrefs: UiSettingsFlow) { - val createDraftsIndex by sharedPrefs.automaticallyCreateDrafts.collectAsState() +private fun BooleanSwitchRow( + flow: MutableStateFlow, + title: Int, + description: Int, +) { + val value by flow.collectAsState() - val booleanItems = - persistentListOf( - TitleExplainer(stringRes(BooleanType.ALWAYS.reourceId)), - TitleExplainer(stringRes(BooleanType.NEVER.reourceId)), + SettingsRow(title, description) { + Switch( + checked = value == BooleanType.ALWAYS, + onCheckedChange = { isOn -> + flow.tryEmit(if (isOn) BooleanType.ALWAYS else BooleanType.NEVER) + }, ) - - SettingsRow( - R.string.auto_create_drafts_setting_title, - R.string.auto_create_drafts_setting_description, - booleanItems, - createDraftsIndex.screenCode, - ) { - sharedPrefs.automaticallyCreateDrafts.tryEmit(parseBooleanType(it)) - } -} - -@Composable -fun AiWritingHelpChoice(sharedPrefs: UiSettingsFlow) { - val aiIndex by sharedPrefs.automaticallyProposeAiImprovements.collectAsState() - - val booleanItems = - persistentListOf( - TitleExplainer(stringRes(ConnectivityType.ALWAYS.resourceId)), - TitleExplainer(stringRes(ConnectivityType.NEVER.resourceId)), - ) - - SettingsRow( - R.string.ai_writing_setting_title, - R.string.ai_writing_setting_description, - booleanItems, - aiIndex.screenCode, - ) { - sharedPrefs.automaticallyProposeAiImprovements.tryEmit(parseBooleanType(it)) - } -} - -@Composable -fun TrackedBroadcastsChoice(sharedPrefs: UiSettingsFlow) { - val useTrackedBroadcastsIndex by sharedPrefs.useTrackedBroadcasts.collectAsState() - - val booleanItems = - persistentListOf( - TitleExplainer(stringRes(BooleanType.ALWAYS.reourceId)), - TitleExplainer(stringRes(BooleanType.NEVER.reourceId)), - ) - - SettingsRow( - R.string.tracked_broadcasts_setting_title, - R.string.tracked_broadcasts_setting_description, - booleanItems, - useTrackedBroadcastsIndex.screenCode, - ) { - sharedPrefs.useTrackedBroadcasts.tryEmit(parseBooleanType(it)) } }