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
This commit is contained in:
@@ -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<NavBarItem> = DefaultBottomBarItems,
|
||||
val showHomeNewThreadsTab: Boolean = true,
|
||||
val showHomeConversationsTab: Boolean = true,
|
||||
|
||||
@@ -43,6 +43,7 @@ class UiSettingsFlow(
|
||||
val gallerySet: MutableStateFlow<ProfileGalleryType> = MutableStateFlow(ProfileGalleryType.CLASSIC),
|
||||
val automaticallyProposeAiImprovements: MutableStateFlow<BooleanType> = MutableStateFlow(BooleanType.ALWAYS),
|
||||
val useTrackedBroadcasts: MutableStateFlow<BooleanType> = MutableStateFlow(BooleanType.ALWAYS),
|
||||
val automaticallyCreateDrafts: MutableStateFlow<BooleanType> = MutableStateFlow(BooleanType.ALWAYS),
|
||||
val bottomBarItems: MutableStateFlow<List<NavBarItem>> = MutableStateFlow(DefaultBottomBarItems),
|
||||
val showHomeNewThreadsTab: MutableStateFlow<Boolean> = MutableStateFlow(true),
|
||||
val showHomeConversationsTab: MutableStateFlow<Boolean> = 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<NavBarItem>,
|
||||
flows[15] as Boolean,
|
||||
flows[14] as BooleanType,
|
||||
flows[15] as List<NavBarItem>,
|
||||
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),
|
||||
|
||||
+3
@@ -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
|
||||
|
||||
@@ -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<Route.Drafts> { DraftListScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.ScheduledPosts> { ScheduledPostsScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.Settings> { SettingsScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.ComposeSettings> { ComposeSettingsScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.UserSettings> { UserSettingsScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.ReactionsSettings> { ReactionsSettingsScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.BottomBarSettings> { BottomBarSettingsScreen(accountViewModel, nav) }
|
||||
|
||||
@@ -215,6 +215,8 @@ sealed class Route {
|
||||
|
||||
@Serializable object Settings : Route()
|
||||
|
||||
@Serializable object ComposeSettings : Route()
|
||||
|
||||
@Serializable object UserSettings : Route()
|
||||
|
||||
@Serializable object ReactionsSettings : Route()
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
@@ -422,6 +422,7 @@ class ChatNewMessageViewModel :
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
if (!accountViewModel.settings.automaticallyCreateDrafts()) return
|
||||
if (message.text.toString().isBlank()) {
|
||||
account.deleteDraftIgnoreErrors(draftTag.current)
|
||||
} else {
|
||||
|
||||
+1
@@ -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 {
|
||||
|
||||
+1
@@ -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 {
|
||||
|
||||
+1
@@ -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 {
|
||||
|
||||
+1
@@ -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 {
|
||||
|
||||
+1
@@ -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 {
|
||||
|
||||
+1
@@ -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 {
|
||||
|
||||
+6
@@ -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,
|
||||
|
||||
-2
@@ -136,8 +136,6 @@ fun SettingsScreen(
|
||||
ImmersiveScrollingChoice(sharedPrefs)
|
||||
FeatureSetChoice(sharedPrefs)
|
||||
GalleryChoice(sharedPrefs)
|
||||
AiWritingHelpChoice(sharedPrefs)
|
||||
TrackedBroadcastsChoice(sharedPrefs)
|
||||
PushNotificationSettingsRow(sharedPrefs)
|
||||
if (accountViewModel != null) {
|
||||
AlwaysOnNotificationServiceChoice(accountViewModel)
|
||||
|
||||
+108
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -2794,6 +2794,9 @@
|
||||
<string name="ai_writing_setting_description">Uses an on-device AI model to propose text corrections and tone changes.</string>
|
||||
<string name="tracked_broadcasts_setting_title">Tracked broadcasts</string>
|
||||
<string name="tracked_broadcasts_setting_description">Use the tracked broadcaster when sending events. Shows live progress and per-relay status while broadcasting.</string>
|
||||
<string name="compose_settings">Compose Settings</string>
|
||||
<string name="auto_create_drafts_setting_title">Automatically create drafts</string>
|
||||
<string name="auto_create_drafts_setting_description">Saves a draft event automatically when you leave a composer with unsent text. Disable to keep your unfinished posts local until you publish.</string>
|
||||
<string name="ai_writing_use_this">Use This</string>
|
||||
<string name="ai_writing_dismiss">Dismiss</string>
|
||||
<string name="ai_tone_correct">Correct</string>
|
||||
|
||||
Reference in New Issue
Block a user