From 60d817b0c01b4bf8193fc8e34306b9344561ccc7 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 16:24:48 +0000 Subject: [PATCH] feat: move AI writing help to Application Preferences Replace the sparkle toggle button in the compose toolbar with a persistent setting in UI/Application Preferences. The new "Propose AI text improvements" setting (Always/Never) controls whether the tone chip row appears in the compose screen. When enabled and the device supports on-device AI, the tone chips show automatically below the text field. Removes the per-session toggle button from BottomRowActions. https://claude.ai/code/session_01RbCYGrbbapRMike8WQy41F --- .../amethyst/model/UiSettings.kt | 1 + .../amethyst/model/UiSettingsFlow.kt | 9 ++++++++ .../model/preferences/UISharedPreferences.kt | 3 +++ .../loggedIn/home/ShortNotePostScreen.kt | 10 +-------- .../loggedIn/home/ShortNotePostViewModel.kt | 10 ++++++++- .../loggedIn/settings/AppSettingsScreen.kt | 21 +++++++++++++++++++ amethyst/src/main/res/values/strings.xml | 2 ++ 7 files changed, 46 insertions(+), 10 deletions(-) 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 b0e6aae20..5b1b09323 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt @@ -38,6 +38,7 @@ data class UiSettings( val dontAskForNotificationPermissions: Boolean = false, val featureSet: FeatureSetType = FeatureSetType.SIMPLIFIED, val gallerySet: ProfileGalleryType = ProfileGalleryType.CLASSIC, + val automaticallyProposeAiImprovements: BooleanType = BooleanType.NEVER, ) enum class ThemeType( 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 467663f7e..3e0ec3222 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt @@ -38,6 +38,7 @@ class UiSettingsFlow( val dontAskForNotificationPermissions: MutableStateFlow = MutableStateFlow(false), val featureSet: MutableStateFlow = MutableStateFlow(FeatureSetType.SIMPLIFIED), val gallerySet: MutableStateFlow = MutableStateFlow(ProfileGalleryType.CLASSIC), + val automaticallyProposeAiImprovements: MutableStateFlow = MutableStateFlow(BooleanType.NEVER), ) { val listOfFlows: List> = listOf>( @@ -52,6 +53,7 @@ class UiSettingsFlow( dontAskForNotificationPermissions, featureSet, gallerySet, + automaticallyProposeAiImprovements, ) // emits at every change in any of the propertyes. @@ -69,6 +71,7 @@ class UiSettingsFlow( flows[8] as Boolean, flows[9] as FeatureSetType, flows[10] as ProfileGalleryType, + flows[11] as BooleanType, ) } @@ -85,6 +88,7 @@ class UiSettingsFlow( dontAskForNotificationPermissions.value, featureSet.value, gallerySet.value, + automaticallyProposeAiImprovements.value, ) fun update(torSettings: UiSettings): Boolean { @@ -134,6 +138,10 @@ class UiSettingsFlow( gallerySet.tryEmit(torSettings.gallerySet) any = true } + if (automaticallyProposeAiImprovements.value != torSettings.automaticallyProposeAiImprovements) { + automaticallyProposeAiImprovements.tryEmit(torSettings.automaticallyProposeAiImprovements) + any = true + } return any } @@ -164,6 +172,7 @@ class UiSettingsFlow( MutableStateFlow(uiSettings.dontAskForNotificationPermissions), MutableStateFlow(uiSettings.featureSet), MutableStateFlow(uiSettings.gallerySet), + MutableStateFlow(uiSettings.automaticallyProposeAiImprovements), ) } } 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 2b673eb36..5dfe07df3 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 @@ -102,6 +102,7 @@ class UiSharedPreferences( val UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS = booleanPreferencesKey("ui.dont_ask_for_notification_permissions") val UI_FEATURE_SET = stringPreferencesKey("ui.feature_set") val UI_GALLERY_SET = stringPreferencesKey("ui.gallery_set") + val UI_PROPOSE_AI_IMPROVEMENTS = stringPreferencesKey("ui.propose_ai_improvements") suspend fun uiPreferences(context: Context): UiSettings? = try { @@ -120,6 +121,7 @@ class UiSharedPreferences( dontAskForNotificationPermissions = preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] ?: false, featureSet = preferences[UI_FEATURE_SET]?.let { FeatureSetType.valueOf(it) } ?: FeatureSetType.SIMPLIFIED, gallerySet = preferences[UI_GALLERY_SET]?.let { ProfileGalleryType.valueOf(it) } ?: ProfileGalleryType.CLASSIC, + automaticallyProposeAiImprovements = preferences[UI_PROPOSE_AI_IMPROVEMENTS]?.let { BooleanType.valueOf(it) } ?: BooleanType.NEVER, ) } catch (e: Exception) { if (e is CancellationException) throw e @@ -155,6 +157,7 @@ class UiSharedPreferences( preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] = sharedSettings.dontAskForNotificationPermissions preferences[UI_FEATURE_SET] = sharedSettings.featureSet.name preferences[UI_GALLERY_SET] = sharedSettings.gallerySet.name + preferences[UI_PROPOSE_AI_IMPROVEMENTS] = sharedSettings.automaticallyProposeAiImprovements.name } } catch (e: Exception) { if (e is CancellationException) throw e diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index c0a116dfe..3758b8d01 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -65,7 +65,6 @@ import androidx.core.net.toUri import androidx.core.util.Consumer import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.service.ai.WritingAssistantStatus import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS @@ -83,7 +82,6 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.ui.note.NoteCompose -import com.vitorpamplona.amethyst.ui.note.creators.aihelp.AiWritingHelpButton import com.vitorpamplona.amethyst.ui.note.creators.aihelp.AiWritingHelpPanel import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton @@ -564,7 +562,7 @@ private fun NewPostScreenBody( } AiWritingHelpPanel( - isVisible = postViewModel.wantsAiHelp, + isVisible = postViewModel.showAiPanel, isProcessing = postViewModel.isAiProcessing, result = postViewModel.aiResult, onToneSelected = postViewModel::requestAiTransform, @@ -667,12 +665,6 @@ private fun BottomRowActions(postViewModel: ShortNotePostViewModel) { postViewModel.wantsInvoice = !postViewModel.wantsInvoice } } - - if (postViewModel.aiStatus is WritingAssistantStatus.Available) { - AiWritingHelpButton(postViewModel.wantsAiHelp) { - postViewModel.wantsAiHelp = !postViewModel.wantsAiHelp - } - } } } 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 cb892662c..fd0a25e32 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 @@ -41,6 +41,7 @@ import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord import com.vitorpamplona.amethyst.commons.compose.setTextAndPlaceCursorAtBeginning import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState.EmojiMedia import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.BooleanType import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -301,12 +302,19 @@ open class ShortNotePostViewModel : // TODO: Remove useMockAi before shipping. Set to true to test UI without Gemini Nano. private val useMockAi = true - var wantsAiHelp by mutableStateOf(false) var aiResult by mutableStateOf(null) var aiStatus by mutableStateOf(WritingAssistantStatus.Unavailable) var isAiProcessing by mutableStateOf(false) private var writingAssistant: WritingAssistant? = null + val showAiPanel: Boolean + get() { + val prefEnabled = + accountViewModel.settings.uiSettingsFlow.automaticallyProposeAiImprovements.value == + BooleanType.ALWAYS + return prefEnabled && aiStatus is WritingAssistantStatus.Available + } + fun initWritingAssistant(context: android.content.Context) { if (writingAssistant == null) { writingAssistant = 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 fc799a4a0..ace4f154d 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 @@ -121,6 +121,7 @@ fun SettingsScreen(sharedPrefs: UiSettingsFlow) { ImmersiveScrollingChoice(sharedPrefs) FeatureSetChoice(sharedPrefs) GalleryChoice(sharedPrefs) + AiWritingHelpChoice(sharedPrefs) PushNotificationSettingsRow(sharedPrefs) } } @@ -366,6 +367,26 @@ 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 SettingsRow( name: Int, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 3548f1e30..b3ba42afb 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -2201,6 +2201,8 @@ Relay leave request AI Writing Help + Propose AI text improvements + Show AI writing help options when composing posts (on-device, requires supported hardware) Use This Dismiss Correct