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 featureSet: FeatureSetType = FeatureSetType.SIMPLIFIED,
val gallerySet: ProfileGalleryType = ProfileGalleryType.CLASSIC,
val automaticallyProposeAiImprovements: BooleanType = BooleanType.NEVER,
)
enum class ThemeType(
@@ -38,6 +38,7 @@ class UiSettingsFlow(
val dontAskForNotificationPermissions: MutableStateFlow<Boolean> = MutableStateFlow(false),
val featureSet: MutableStateFlow<FeatureSetType> = MutableStateFlow(FeatureSetType.SIMPLIFIED),
val gallerySet: MutableStateFlow<ProfileGalleryType> = MutableStateFlow(ProfileGalleryType.CLASSIC),
val automaticallyProposeAiImprovements: MutableStateFlow<BooleanType> = MutableStateFlow(BooleanType.NEVER),
) {
val listOfFlows: List<Flow<Any?>> =
listOf<Flow<Any?>>(
@@ -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),
)
}
}
@@ -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
@@ -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
}
}
}
}
@@ -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<WritingResult?>(null)
var aiStatus by mutableStateOf<WritingAssistantStatus>(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 =
@@ -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,
+2
View File
@@ -2201,6 +2201,8 @@
<string name="relay_leave_request">Relay leave request</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_dismiss">Dismiss</string>
<string name="ai_tone_correct">Correct</string>