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 3393bd4aa9..154bd40fd6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt @@ -42,6 +42,7 @@ data class UiSettings( val featureSet: FeatureSetType = FeatureSetType.SIMPLIFIED, val gallerySet: ProfileGalleryType = ProfileGalleryType.CLASSIC, val automaticallyProposeAiImprovements: BooleanType = BooleanType.ALWAYS, + val useTrackedBroadcasts: BooleanType = BooleanType.ALWAYS, val bottomBarItems: List = DefaultBottomBarItems, ) 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 d4a5144eeb..b7bcb17c34 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt @@ -42,6 +42,7 @@ class UiSettingsFlow( val featureSet: MutableStateFlow = MutableStateFlow(FeatureSetType.SIMPLIFIED), val gallerySet: MutableStateFlow = MutableStateFlow(ProfileGalleryType.CLASSIC), val automaticallyProposeAiImprovements: MutableStateFlow = MutableStateFlow(BooleanType.ALWAYS), + val useTrackedBroadcasts: MutableStateFlow = MutableStateFlow(BooleanType.ALWAYS), val bottomBarItems: MutableStateFlow> = MutableStateFlow(DefaultBottomBarItems), ) { val listOfFlows: List> = @@ -59,6 +60,7 @@ class UiSettingsFlow( featureSet, gallerySet, automaticallyProposeAiImprovements, + useTrackedBroadcasts, bottomBarItems, ) @@ -80,7 +82,8 @@ class UiSettingsFlow( flows[10] as FeatureSetType, flows[11] as ProfileGalleryType, flows[12] as BooleanType, - flows[13] as List, + flows[13] as BooleanType, + flows[14] as List, ) } @@ -99,6 +102,7 @@ class UiSettingsFlow( featureSet.value, gallerySet.value, automaticallyProposeAiImprovements.value, + useTrackedBroadcasts.value, bottomBarItems.value, ) @@ -157,6 +161,10 @@ class UiSettingsFlow( automaticallyProposeAiImprovements.tryEmit(torSettings.automaticallyProposeAiImprovements) any = true } + if (useTrackedBroadcasts.value != torSettings.useTrackedBroadcasts) { + useTrackedBroadcasts.tryEmit(torSettings.useTrackedBroadcasts) + any = true + } if (bottomBarItems.value != torSettings.bottomBarItems) { bottomBarItems.tryEmit(torSettings.bottomBarItems) any = true @@ -193,6 +201,7 @@ class UiSettingsFlow( MutableStateFlow(uiSettings.featureSet), MutableStateFlow(uiSettings.gallerySet), MutableStateFlow(uiSettings.automaticallyProposeAiImprovements), + MutableStateFlow(uiSettings.useTrackedBroadcasts), MutableStateFlow(uiSettings.bottomBarItems), ) } 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 586bacb81b..28be3d53e4 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 @@ -106,6 +106,7 @@ class UiSharedPreferences( 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") + val UI_USE_TRACKED_BROADCASTS = stringPreferencesKey("ui.use_tracked_broadcasts") val UI_BOTTOM_BAR_ITEMS = stringPreferencesKey("ui.bottom_bar_items") suspend fun uiPreferences(context: Context): UiSettings? = @@ -113,6 +114,8 @@ class UiSharedPreferences( // Get the preference flow and take the first value. val preferences = context.sharedPreferencesDataStore.data.first() + val featureSet = preferences[UI_FEATURE_SET]?.let { FeatureSetType.valueOf(it) } ?: FeatureSetType.SIMPLIFIED + UiSettings( theme = preferences[UI_THEME]?.let { ThemeType.valueOf(it) } ?: ThemeType.SYSTEM, preferredLanguage = preferences[UI_LANGUAGE]?.ifBlank { null }, @@ -124,9 +127,12 @@ class UiSharedPreferences( automaticallyShowProfilePictures = preferences[UI_SHOW_PROFILE_PICTURES]?.let { ConnectivityType.valueOf(it) } ?: ConnectivityType.ALWAYS, dontShowPushNotificationSelector = preferences[UI_DONT_SHOW_PUSH_NOTIFICATION_SELECTOR] ?: false, dontAskForNotificationPermissions = preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] ?: false, - featureSet = preferences[UI_FEATURE_SET]?.let { FeatureSetType.valueOf(it) } ?: FeatureSetType.SIMPLIFIED, + featureSet = featureSet, gallerySet = preferences[UI_GALLERY_SET]?.let { ProfileGalleryType.valueOf(it) } ?: ProfileGalleryType.CLASSIC, automaticallyProposeAiImprovements = preferences[UI_PROPOSE_AI_IMPROVEMENTS]?.let { BooleanType.valueOf(it) } ?: BooleanType.ALWAYS, + useTrackedBroadcasts = + preferences[UI_USE_TRACKED_BROADCASTS]?.let { BooleanType.valueOf(it) } + ?: if (featureSet == FeatureSetType.COMPLETE) BooleanType.ALWAYS else BooleanType.NEVER, bottomBarItems = preferences[UI_BOTTOM_BAR_ITEMS]?.let { decodeBottomBarItems(it) } ?: DefaultBottomBarItems, ) } catch (e: Exception) { @@ -165,6 +171,7 @@ class UiSharedPreferences( preferences[UI_FEATURE_SET] = sharedSettings.featureSet.name 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_BOTTOM_BAR_ITEMS] = sharedSettings.bottomBarItems.joinToString(",") { it.name } } } catch (e: Exception) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/DisplayBroadcastProgress.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/DisplayBroadcastProgress.kt index 1eb2253744..58d8ac4b60 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/DisplayBroadcastProgress.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/DisplayBroadcastProgress.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.model.BooleanType import com.vitorpamplona.amethyst.service.broadcast.BroadcastEvent import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -49,13 +50,14 @@ import kotlinx.coroutines.delay * - CompletedBroadcastIndicator: Shows completed broadcast for tap-to-view (auto-dismisses after 10s) * - BroadcastDetailsSheet: Shows detailed relay status on tap * - * Only shown when FeatureSetType.COMPLETE is enabled. + * Hidden when the "Tracked broadcasts" UI setting is off. */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun DisplayBroadcastProgress(accountViewModel: AccountViewModel) { - // Only show in COMPLETE UI mode - if (!accountViewModel.settings.isCompleteUIMode()) return + val useTrackedBroadcasts by accountViewModel.settings.uiSettingsFlow.useTrackedBroadcasts + .collectAsStateWithLifecycle() + if (useTrackedBroadcasts != BooleanType.ALWAYS) return val activeBroadcasts by accountViewModel.broadcastTracker.activeBroadcasts.collectAsStateWithLifecycle() 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 ef3a608d26..bd28f91bf7 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 @@ -131,6 +131,8 @@ class UiSettingsState( fun isCompleteUIMode() = uiSettingsFlow.featureSet.value == FeatureSetType.COMPLETE + fun useTrackedBroadcasts() = uiSettingsFlow.useTrackedBroadcasts.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/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index f6989dd7f3..c944b071f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -413,7 +413,7 @@ class AccountViewModel( if (currentReactions.isNotEmpty()) { account.delete(currentReactions) } else { - if (settings.isCompleteUIMode() && note.event !is NIP17Group) { + if (settings.useTrackedBroadcasts() && note.event !is NIP17Group) { // Tracked broadcasting with progress feedback account.createReactionEvent(note, reaction)?.let { (event, relays) -> broadcastTracker.trackBroadcast( @@ -841,24 +841,12 @@ class AccountViewModel( } } - fun boost(note: Note) { - if (settings.isCompleteUIMode()) { - // Tracked broadcasting with progress feedback - launchSigner { - account.createBoostEvent(note)?.let { (event, relays) -> - broadcastTracker.trackBroadcast( - event = event, - relays = relays, - client = account.client, - ) - account.consumeBoostEvent(event) - } - } - } else { - // Fire-and-forget (original behavior) - launchSigner { account.boost(note) } - } - } + fun boost(note: Note) = + launchTrackedOrDirect( + createTracked = { account.createBoostEvent(note) }, + consumeTracked = account::consumeBoostEvent, + direct = { account.boost(note) }, + ) fun removeEmojiPack(emojiPack: Note) = launchSigner { account.removeEmojiPack(emojiPack) } @@ -886,112 +874,51 @@ class AccountViewModel( fun pinnedNotes(user: User): Note = LocalCache.getOrCreateAddressableNote(PinListEvent.createPinAddress(user.pubkeyHex)) - fun addPin(note: Note) { - if (settings.isCompleteUIMode()) { - launchSigner { - account.createAddPinEvent(note)?.let { (event, relays) -> - broadcastTracker.trackBroadcast( - event = event, - relays = relays, - client = account.client, - ) - account.consumePinEvent(event) - } - } - } else { - launchSigner { account.addPin(note) } - } - } + fun addPin(note: Note) = + launchTrackedOrDirect( + createTracked = { account.createAddPinEvent(note) }, + consumeTracked = account::consumePinEvent, + direct = { account.addPin(note) }, + ) - fun removePin(note: Note) { - if (settings.isCompleteUIMode()) { - launchSigner { - account.createRemovePinEvent(note)?.let { (event, relays) -> - broadcastTracker.trackBroadcast( - event = event, - relays = relays, - client = account.client, - ) - account.consumePinEvent(event) - } - } - } else { - launchSigner { account.removePin(note) } - } - } + fun removePin(note: Note) = + launchTrackedOrDirect( + createTracked = { account.createRemovePinEvent(note) }, + consumeTracked = account::consumePinEvent, + direct = { account.removePin(note) }, + ) fun removeDeletedPins(deletedNotes: Set) { launchSigner { account.removeDeletedPins(deletedNotes) } } - fun addPrivateBookmark(note: Note) { - if (settings.isCompleteUIMode()) { - launchSigner { - account.createAddBookmarkEvent(note, true)?.let { (event, relays) -> - broadcastTracker.trackBroadcast( - event = event, - relays = relays, - client = account.client, - ) - account.consumeBookmarkEvent(event) - } - } - } else { - launchSigner { account.addBookmark(note, true) } - } - } + fun addPrivateBookmark(note: Note) = + launchTrackedOrDirect( + createTracked = { account.createAddBookmarkEvent(note, true) }, + consumeTracked = account::consumeBookmarkEvent, + direct = { account.addBookmark(note, true) }, + ) - fun addPublicBookmark(note: Note) { - if (settings.isCompleteUIMode()) { - launchSigner { - account.createAddBookmarkEvent(note, false)?.let { (event, relays) -> - broadcastTracker.trackBroadcast( - event = event, - relays = relays, - client = account.client, - ) - account.consumeBookmarkEvent(event) - } - } - } else { - launchSigner { account.addBookmark(note, false) } - } - } + fun addPublicBookmark(note: Note) = + launchTrackedOrDirect( + createTracked = { account.createAddBookmarkEvent(note, false) }, + consumeTracked = account::consumeBookmarkEvent, + direct = { account.addBookmark(note, false) }, + ) - fun removePrivateBookmark(note: Note) { - if (settings.isCompleteUIMode()) { - launchSigner { - account.createRemoveBookmarkEvent(note, true)?.let { (event, relays) -> - broadcastTracker.trackBroadcast( - event = event, - relays = relays, - client = account.client, - ) + fun removePrivateBookmark(note: Note) = + launchTrackedOrDirect( + createTracked = { account.createRemoveBookmarkEvent(note, true) }, + consumeTracked = account::consumeBookmarkEvent, + direct = { account.removeBookmark(note, true) }, + ) - account.consumeBookmarkEvent(event) - } - } - } else { - launchSigner { account.removeBookmark(note, true) } - } - } - - fun removePublicBookmark(note: Note) { - if (settings.isCompleteUIMode()) { - launchSigner { - account.createRemoveBookmarkEvent(note, false)?.let { (event, relays) -> - broadcastTracker.trackBroadcast( - event = event, - relays = relays, - client = account.client, - ) - account.consumeBookmarkEvent(event) - } - } - } else { - launchSigner { account.removeBookmark(note, false) } - } - } + fun removePublicBookmark(note: Note) = + launchTrackedOrDirect( + createTracked = { account.createRemoveBookmarkEvent(note, false) }, + consumeTracked = account::consumeBookmarkEvent, + direct = { account.removeBookmark(note, false) }, + ) fun removeDeletedBookmarks( deletedEventIds: Set, @@ -1035,6 +962,29 @@ class AccountViewModel( account.decryptContent(note)?.let { onReady(it) } } + /** + * Runs an action that has both a tracked and a direct broadcast variant, + * picking the path the user selected via the "Tracked broadcasts" setting. + */ + inline fun launchTrackedOrDirect( + crossinline createTracked: suspend () -> Pair>?, + crossinline consumeTracked: (Event) -> Unit, + crossinline direct: suspend () -> Unit, + ) = launchSigner { + if (settings.useTrackedBroadcasts()) { + createTracked()?.let { (event, relays) -> + broadcastTracker.trackBroadcast( + event = event, + relays = relays, + client = account.client, + ) + consumeTracked(event) + } + } else { + direct() + } + } + inline fun launchSigner(crossinline action: suspend () -> Unit) { viewModelScope.launch(Dispatchers.IO) { try { 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 ee865aea33..972ed92cc5 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 @@ -326,7 +326,7 @@ class LongFormPostViewModel : val version = draftTag.current cancel() - if (accountViewModel.settings.isCompleteUIMode()) { + if (accountViewModel.settings.useTrackedBroadcasts()) { val (event, relays, extras) = accountViewModel.account.createPostEvent(template, emptyList()) accountViewModel.viewModelScope.launch(Dispatchers.IO) { accountViewModel.broadcastTracker.trackBroadcast( 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 a5bc0c5684..e92b1ac831 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 @@ -833,7 +833,7 @@ open class ShortNotePostViewModel : if (anonymous) { accountViewModel.account.signAnonymouslyAndBroadcast(template, extraNotesToBroadcast) - } else if (accountViewModel.settings.isCompleteUIMode()) { + } else if (accountViewModel.settings.useTrackedBroadcasts()) { // Tracked broadcasting with progress feedback (non-blocking) val (event, relays, extras) = accountViewModel.account.createPostEvent(template, extraNotesToBroadcast) 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 9a18876d9e..b4e4468eab 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 @@ -54,6 +54,7 @@ import androidx.compose.ui.unit.dp import androidx.core.os.LocaleListCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle 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 @@ -133,6 +134,7 @@ fun SettingsScreen( FeatureSetChoice(sharedPrefs) GalleryChoice(sharedPrefs) AiWritingHelpChoice(sharedPrefs) + TrackedBroadcastsChoice(sharedPrefs) PushNotificationSettingsRow(sharedPrefs) if (accountViewModel != null) { AlwaysOnNotificationServiceChoice(accountViewModel) @@ -421,6 +423,26 @@ fun AiWritingHelpChoice(sharedPrefs: UiSettingsFlow) { } } +@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/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 4416206f96..50dbf44807 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -2469,6 +2469,8 @@ AI Writing Help Propose text improvements 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. Use This Dismiss Correct