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
This commit is contained in:
Claude
2026-04-08 16:24:48 +00:00
parent e674141054
commit 60d817b0c0
7 changed files with 46 additions and 10 deletions
@@ -38,6 +38,7 @@ data class UiSettings(
val dontAskForNotificationPermissions: Boolean = false, val dontAskForNotificationPermissions: Boolean = false,
val featureSet: FeatureSetType = FeatureSetType.SIMPLIFIED, val featureSet: FeatureSetType = FeatureSetType.SIMPLIFIED,
val gallerySet: ProfileGalleryType = ProfileGalleryType.CLASSIC, val gallerySet: ProfileGalleryType = ProfileGalleryType.CLASSIC,
val automaticallyProposeAiImprovements: BooleanType = BooleanType.NEVER,
) )
enum class ThemeType( enum class ThemeType(
@@ -38,6 +38,7 @@ class UiSettingsFlow(
val dontAskForNotificationPermissions: MutableStateFlow<Boolean> = MutableStateFlow(false), val dontAskForNotificationPermissions: MutableStateFlow<Boolean> = MutableStateFlow(false),
val featureSet: MutableStateFlow<FeatureSetType> = MutableStateFlow(FeatureSetType.SIMPLIFIED), val featureSet: MutableStateFlow<FeatureSetType> = MutableStateFlow(FeatureSetType.SIMPLIFIED),
val gallerySet: MutableStateFlow<ProfileGalleryType> = MutableStateFlow(ProfileGalleryType.CLASSIC), val gallerySet: MutableStateFlow<ProfileGalleryType> = MutableStateFlow(ProfileGalleryType.CLASSIC),
val automaticallyProposeAiImprovements: MutableStateFlow<BooleanType> = MutableStateFlow(BooleanType.NEVER),
) { ) {
val listOfFlows: List<Flow<Any?>> = val listOfFlows: List<Flow<Any?>> =
listOf<Flow<Any?>>( listOf<Flow<Any?>>(
@@ -52,6 +53,7 @@ class UiSettingsFlow(
dontAskForNotificationPermissions, dontAskForNotificationPermissions,
featureSet, featureSet,
gallerySet, gallerySet,
automaticallyProposeAiImprovements,
) )
// emits at every change in any of the propertyes. // emits at every change in any of the propertyes.
@@ -69,6 +71,7 @@ class UiSettingsFlow(
flows[8] as Boolean, flows[8] as Boolean,
flows[9] as FeatureSetType, flows[9] as FeatureSetType,
flows[10] as ProfileGalleryType, flows[10] as ProfileGalleryType,
flows[11] as BooleanType,
) )
} }
@@ -85,6 +88,7 @@ class UiSettingsFlow(
dontAskForNotificationPermissions.value, dontAskForNotificationPermissions.value,
featureSet.value, featureSet.value,
gallerySet.value, gallerySet.value,
automaticallyProposeAiImprovements.value,
) )
fun update(torSettings: UiSettings): Boolean { fun update(torSettings: UiSettings): Boolean {
@@ -134,6 +138,10 @@ class UiSettingsFlow(
gallerySet.tryEmit(torSettings.gallerySet) gallerySet.tryEmit(torSettings.gallerySet)
any = true any = true
} }
if (automaticallyProposeAiImprovements.value != torSettings.automaticallyProposeAiImprovements) {
automaticallyProposeAiImprovements.tryEmit(torSettings.automaticallyProposeAiImprovements)
any = true
}
return any return any
} }
@@ -164,6 +172,7 @@ class UiSettingsFlow(
MutableStateFlow(uiSettings.dontAskForNotificationPermissions), MutableStateFlow(uiSettings.dontAskForNotificationPermissions),
MutableStateFlow(uiSettings.featureSet), MutableStateFlow(uiSettings.featureSet),
MutableStateFlow(uiSettings.gallerySet), MutableStateFlow(uiSettings.gallerySet),
MutableStateFlow(uiSettings.automaticallyProposeAiImprovements),
) )
} }
} }
@@ -102,6 +102,7 @@ class UiSharedPreferences(
val UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS = booleanPreferencesKey("ui.dont_ask_for_notification_permissions") val UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS = booleanPreferencesKey("ui.dont_ask_for_notification_permissions")
val UI_FEATURE_SET = stringPreferencesKey("ui.feature_set") val UI_FEATURE_SET = stringPreferencesKey("ui.feature_set")
val UI_GALLERY_SET = stringPreferencesKey("ui.gallery_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? = suspend fun uiPreferences(context: Context): UiSettings? =
try { try {
@@ -120,6 +121,7 @@ class UiSharedPreferences(
dontAskForNotificationPermissions = preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] ?: false, dontAskForNotificationPermissions = preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] ?: false,
featureSet = preferences[UI_FEATURE_SET]?.let { FeatureSetType.valueOf(it) } ?: FeatureSetType.SIMPLIFIED, featureSet = preferences[UI_FEATURE_SET]?.let { FeatureSetType.valueOf(it) } ?: FeatureSetType.SIMPLIFIED,
gallerySet = preferences[UI_GALLERY_SET]?.let { ProfileGalleryType.valueOf(it) } ?: ProfileGalleryType.CLASSIC, 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) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
@@ -155,6 +157,7 @@ class UiSharedPreferences(
preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] = sharedSettings.dontAskForNotificationPermissions preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] = sharedSettings.dontAskForNotificationPermissions
preferences[UI_FEATURE_SET] = sharedSettings.featureSet.name preferences[UI_FEATURE_SET] = sharedSettings.featureSet.name
preferences[UI_GALLERY_SET] = sharedSettings.gallerySet.name preferences[UI_GALLERY_SET] = sharedSettings.gallerySet.name
preferences[UI_PROPOSE_AI_IMPROVEMENTS] = sharedSettings.automaticallyProposeAiImprovements.name
} }
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
@@ -65,7 +65,6 @@ import androidx.core.net.toUri
import androidx.core.util.Consumer import androidx.core.util.Consumer
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R 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.StrippingFailureDialog
import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow
import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS 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.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
import com.vitorpamplona.amethyst.ui.note.NoteCompose 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.aihelp.AiWritingHelpPanel
import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer
import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton
@@ -564,7 +562,7 @@ private fun NewPostScreenBody(
} }
AiWritingHelpPanel( AiWritingHelpPanel(
isVisible = postViewModel.wantsAiHelp, isVisible = postViewModel.showAiPanel,
isProcessing = postViewModel.isAiProcessing, isProcessing = postViewModel.isAiProcessing,
result = postViewModel.aiResult, result = postViewModel.aiResult,
onToneSelected = postViewModel::requestAiTransform, onToneSelected = postViewModel::requestAiTransform,
@@ -667,12 +665,6 @@ private fun BottomRowActions(postViewModel: ShortNotePostViewModel) {
postViewModel.wantsInvoice = !postViewModel.wantsInvoice postViewModel.wantsInvoice = !postViewModel.wantsInvoice
} }
} }
if (postViewModel.aiStatus is WritingAssistantStatus.Available) {
AiWritingHelpButton(postViewModel.wantsAiHelp) {
postViewModel.wantsAiHelp = !postViewModel.wantsAiHelp
}
}
} }
} }
@@ -41,6 +41,7 @@ import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord
import com.vitorpamplona.amethyst.commons.compose.setTextAndPlaceCursorAtBeginning import com.vitorpamplona.amethyst.commons.compose.setTextAndPlaceCursorAtBeginning
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState.EmojiMedia import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState.EmojiMedia
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.BooleanType
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User 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. // TODO: Remove useMockAi before shipping. Set to true to test UI without Gemini Nano.
private val useMockAi = true private val useMockAi = true
var wantsAiHelp by mutableStateOf(false)
var aiResult by mutableStateOf<WritingResult?>(null) var aiResult by mutableStateOf<WritingResult?>(null)
var aiStatus by mutableStateOf<WritingAssistantStatus>(WritingAssistantStatus.Unavailable) var aiStatus by mutableStateOf<WritingAssistantStatus>(WritingAssistantStatus.Unavailable)
var isAiProcessing by mutableStateOf(false) var isAiProcessing by mutableStateOf(false)
private var writingAssistant: WritingAssistant? = null 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) { fun initWritingAssistant(context: android.content.Context) {
if (writingAssistant == null) { if (writingAssistant == null) {
writingAssistant = writingAssistant =
@@ -121,6 +121,7 @@ fun SettingsScreen(sharedPrefs: UiSettingsFlow) {
ImmersiveScrollingChoice(sharedPrefs) ImmersiveScrollingChoice(sharedPrefs)
FeatureSetChoice(sharedPrefs) FeatureSetChoice(sharedPrefs)
GalleryChoice(sharedPrefs) GalleryChoice(sharedPrefs)
AiWritingHelpChoice(sharedPrefs)
PushNotificationSettingsRow(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 @Composable
fun SettingsRow( fun SettingsRow(
name: Int, name: Int,
+2
View File
@@ -2201,6 +2201,8 @@
<string name="relay_leave_request">Relay leave request</string> <string name="relay_leave_request">Relay leave request</string>
<string name="ai_writing_help">AI Writing Help</string> <string name="ai_writing_help">AI Writing Help</string>
<string name="ai_writing_setting_title">Propose AI text improvements</string>
<string name="ai_writing_setting_description">Show AI writing help options when composing posts (on-device, requires supported hardware)</string>
<string name="ai_writing_use_this">Use This</string> <string name="ai_writing_use_this">Use This</string>
<string name="ai_writing_dismiss">Dismiss</string> <string name="ai_writing_dismiss">Dismiss</string>
<string name="ai_tone_correct">Correct</string> <string name="ai_tone_correct">Correct</string>