diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt index 2367c9f75..1a8821043 100644 --- a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt +++ b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.components +import android.Manifest +import android.os.Build import android.util.Log import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues @@ -36,6 +38,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -44,8 +47,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser import com.halilibo.richtext.commonmark.MarkdownParseOptions import com.halilibo.richtext.markdown.BasicMarkdown @@ -53,11 +58,10 @@ import com.halilibo.richtext.ui.RichTextStyle import com.halilibo.richtext.ui.material3.RichText import com.halilibo.richtext.ui.resolveDefaults import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.service.notifications.PushDistributorHandler import com.vitorpamplona.amethyst.ui.components.SpinnerSelectionDialog import com.vitorpamplona.amethyst.ui.components.TitleExplainer -import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.checkifItNeedsToRequestNotificationPermission import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.ui.stringRes import kotlinx.collections.immutable.ImmutableList @@ -65,12 +69,31 @@ import kotlinx.collections.immutable.toImmutableList @OptIn(ExperimentalPermissionsApi::class) @Composable -fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesViewModel) { - val notificationPermissionState = - checkifItNeedsToRequestNotificationPermission(sharedPreferencesViewModel) +fun SelectNotificationProvider(sharedPrefs: UiSettingsFlow) { + val notificationPermissionGranted = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val notificationPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) - if (notificationPermissionState.status.isGranted) { - if (!sharedPreferencesViewModel.sharedPrefs.dontShowPushNotificationSelector) { + if (!notificationPermissionState.status.isGranted) { + val dontAskForNotificationPermissions by sharedPrefs.dontAskForNotificationPermissions.collectAsStateWithLifecycle() + if (!dontAskForNotificationPermissions) { + sharedPrefs.dontAskForNotificationPermissions() + + // This will pause the APP, including the connection with relays. + LaunchedEffect(notificationPermissionState) { + notificationPermissionState.launchPermissionRequest() + } + } + } + + notificationPermissionState.status.isGranted + } else { + true + } + + if (notificationPermissionGranted) { + val pushNotificationSelector by sharedPrefs.dontShowPushNotificationSelector.collectAsStateWithLifecycle() + if (!pushNotificationSelector) { val context = LocalContext.current var distributorPresent by remember { mutableStateOf(PushDistributorHandler.savedDistributorExists()) @@ -84,8 +107,8 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView onSelect = { index -> if (list[index] == "None") { PushDistributorHandler.forceRemoveDistributor(context) - sharedPreferencesViewModel.dontAskForNotificationPermissions() - sharedPreferencesViewModel.dontShowPushNotificationSelector() + sharedPrefs.dontAskForNotificationPermissions() + sharedPrefs.dontShowPushNotificationSelector() } else { val fullDistributorName = list[index] PushDistributorHandler.saveDistributor(fullDistributorName) @@ -125,7 +148,7 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView TextButton( onClick = { distributorPresent = true - sharedPreferencesViewModel.dontShowPushNotificationSelector() + sharedPrefs.dontShowPushNotificationSelector() }, ) { Text(stringRes(R.string.quick_action_dont_show_again_button)) @@ -187,7 +210,7 @@ fun LoadDistributors(onInner: @Composable (String, ImmutableList, Immuta } @Composable -fun PushNotificationSettingsRow(sharedPreferencesViewModel: SharedPreferencesViewModel) { +fun PushNotificationSettingsRow(sharedPrefs: UiSettingsFlow) { val context = LocalContext.current LoadDistributors { currentDistributor, list, readableListWithExplainer -> @@ -198,8 +221,8 @@ fun PushNotificationSettingsRow(sharedPreferencesViewModel: SharedPreferencesVie selectedIndex = list.indexOf(currentDistributor), ) { index -> if (list[index] == "None") { - sharedPreferencesViewModel.dontAskForNotificationPermissions() - sharedPreferencesViewModel.dontShowPushNotificationSelector() + sharedPrefs.dontAskForNotificationPermissions() + sharedPrefs.dontShowPushNotificationSelector() PushDistributorHandler.forceRemoveDistributor(context) } else { PushDistributorHandler.saveDistributor(list[index]) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index 234325e73..7ae55fbab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -21,210 +21,33 @@ package com.vitorpamplona.amethyst import android.app.Application -import android.content.ContentResolver import android.util.Log -import androidx.security.crypto.EncryptedSharedPreferences -import coil3.disk.DiskCache -import coil3.memory.MemoryCache -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState -import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager -import com.vitorpamplona.amethyst.service.crashreports.CrashReportCache -import com.vitorpamplona.amethyst.service.crashreports.UnexpectedCrashSaver -import com.vitorpamplona.amethyst.service.eventCache.MemoryTrimmingService -import com.vitorpamplona.amethyst.service.images.ImageCacheFactory -import com.vitorpamplona.amethyst.service.images.ImageLoaderSetup -import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.logging.Logging -import com.vitorpamplona.amethyst.service.notifications.PokeyReceiver -import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager -import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache -import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket -import com.vitorpamplona.amethyst.service.okhttp.ProxySettingsAnchor -import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache -import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCacheFactory -import com.vitorpamplona.amethyst.service.relayClient.CacheClientConnector -import com.vitorpamplona.amethyst.service.relayClient.RelayProxyClientConnector -import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.AuthCoordinator -import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.NotifyCoordinator -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator -import com.vitorpamplona.amethyst.service.relayClient.speedLogger.RelaySpeedLogger -import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory -import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.scope -import com.vitorpamplona.amethyst.ui.tor.TorManager -import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient -import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache -import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OtsBlockHeightCache -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import java.io.File class Amethyst : Application() { - val appAgent = "Amethyst/${BuildConfig.VERSION_NAME}" - - // Exists to avoid exceptions stopping the coroutine - val exceptionHandler = - CoroutineExceptionHandler { _, throwable -> - Log.e("AmethystCoroutine", "Caught exception: ${throwable.message}", throwable) - } - - val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler) - - // Key cache service to download and decrypt encrypted files before caching them. - val keyCache = EncryptionKeyCache() - - // App services that should be run as soon as there are subscribers to their flows - val locationManager = LocationState(this, applicationIOScope) - val connManager = ConnectivityManager(this, applicationIOScope) - - val torManager = TorManager(this, applicationIOScope) - - // Service that will run at all times to receive events from Pokey - val pokeyReceiver = PokeyReceiver() - - // manages all the other connections separately from relays. - val okHttpClients = - DualHttpClientManager( - userAgent = appAgent, - proxyPortProvider = torManager.activePortOrNull, - isMobileDataProvider = connManager.isMobileOrNull, - keyCache = keyCache, - scope = applicationIOScope, - ) - - // manages all relay connections - val okHttpClientForRelays = - DualHttpClientManager( - userAgent = appAgent, - proxyPortProvider = torManager.activePortOrNull, - isMobileDataProvider = connManager.isMobileOrNull, - keyCache = keyCache, - scope = applicationIOScope, - ) - - // manages all relay connections - val okHttpClientForRelaysForDms = - DualHttpClientManager( - userAgent = appAgent, - proxyPortProvider = torManager.activePortOrNull, - isMobileDataProvider = connManager.isMobileOrNull, - keyCache = keyCache, - scope = applicationIOScope, - ) - - val torProxySettingsAnchor = ProxySettingsAnchor() - - // Connects the NostrClient class with okHttp - val websocketBuilder = - OkHttpWebSocket.Builder { url -> - val useTor = torProxySettingsAnchor.useProxy(url) - if (torProxySettingsAnchor.isDM(url)) { - okHttpClientForRelaysForDms.getHttpClient(useTor) - } else { - okHttpClientForRelays.getHttpClient(useTor) - } - } - - // Caches all events in Memory - val cache: LocalCache = LocalCache - - // Organizes cache clearing - val trimmingService = MemoryTrimmingService(cache) - - // Provides a relay pool - val client: INostrClient = NostrClient(websocketBuilder, applicationIOScope) - - // Watches for changes on Tor and Relay List Settings - val relayProxyClientConnector = RelayProxyClientConnector(torProxySettingsAnchor, okHttpClients, connManager, client, torManager, applicationIOScope) - - // Verifies and inserts in the cache from all relays, all subscriptions - val cacheClientConnector = CacheClientConnector(client, cache) - - // Show messages from the Relay and controls their dismissal - val notifyCoordinator = NotifyCoordinator(client) - - // Authenticates with relays. - val authCoordinator = AuthCoordinator(client, applicationIOScope) - - val logger = if (isDebug) RelaySpeedLogger(client) else null - - // Coordinates all subscriptions for the Nostr Client - val sources: RelaySubscriptionsCoordinator = RelaySubscriptionsCoordinator(LocalCache, client, applicationIOScope) - - // keeps all accounts live - val accountsCache = - AccountCacheState( - geolocationFlow = locationManager.geohashStateFlow, - nwcFilterAssembler = sources.nwc, - contentResolverFn = ::contentResolverFn, - cache = cache, - client = client, - ) - - // saves the .content of NIP-95 blobs in disk to save memory - val nip95cache: File by lazy { Nip95CacheFactory.new(this) } - - // local video cache with disk + memory - val videoCache: VideoCache by lazy { VideoCacheFactory.new(this) } - - // image cache in disk for coil - val diskCache: DiskCache by lazy { ImageCacheFactory.newDisk(this) } - - // image cache in memory for coil - val memoryCache: MemoryCache by lazy { ImageCacheFactory.newMemory(this) } - - val crashReportCache: CrashReportCache by lazy { CrashReportCache(this) } - - // Application-wide ots verification cache - val otsVerifCache by lazy { VerificationStateCache() } - - // Application-wide block height request cache - val otsBlockHeightCache by lazy { OtsBlockHeightCache() } + companion object { + lateinit var instance: AppModules + private set + } override fun onCreate() { super.onCreate() Log.d("AmethystApp", "onCreate $this") - instance = this - - Thread.setDefaultUncaughtExceptionHandler(UnexpectedCrashSaver(crashReportCache, applicationIOScope)) + instance = AppModules(this) if (isDebug) { Logging.setup() } - // initializes diskcache on an IO thread. - applicationIOScope.launch { - diskCache - videoCache - } - - // registers to receive events - pokeyReceiver.register(this) + instance.initiate(this) } override fun onTerminate() { super.onTerminate() Log.d("AmethystApp", "onTerminate $this") - - pokeyReceiver.unregister(this) - applicationIOScope.cancel("Application onTerminate $this") + instance.terminate(this) } - fun contentResolverFn(): ContentResolver = contentResolver - - fun setImageLoader(shouldUseTor: (String) -> Boolean?) { - ImageLoaderSetup.setup(this, diskCache, memoryCache) { url -> - shouldUseTor(url)?.let { okHttpClients.getHttpClient(it) } ?: okHttpClients.getHttpClient(false) - } - } - - fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences = EncryptedStorage.preferences(instance, npub) - /** * Release memory when the UI becomes hidden or when system resources become low. * @@ -233,13 +56,6 @@ class Amethyst : Application() { override fun onTrimMemory(level: Int) { super.onTrimMemory(level) Log.d("AmethystApp", "onTrimMemory $level") - applicationIOScope.launch(Dispatchers.Default) { - trimmingService.run(null, LocalPreferences.allSavedAccounts()) - } - } - - companion object { - lateinit var instance: Amethyst - private set + instance.trim() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt new file mode 100644 index 000000000..2f1f3b9e8 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt @@ -0,0 +1,289 @@ +/** + * 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 + +import android.content.ContentResolver +import android.content.Context +import android.util.Log +import androidx.security.crypto.EncryptedSharedPreferences +import coil3.disk.DiskCache +import coil3.memory.MemoryCache +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState +import com.vitorpamplona.amethyst.model.nip03Timestamp.IncomingOtsEventVerifier +import com.vitorpamplona.amethyst.model.nip11RelayInfo.Nip11CachedRetriever +import com.vitorpamplona.amethyst.model.preferences.TorSharedPreferences +import com.vitorpamplona.amethyst.model.preferences.UiSharedPreferences +import com.vitorpamplona.amethyst.model.privacyOptions.RoleBasedHttpClientBuilder +import com.vitorpamplona.amethyst.model.torState.AccountsTorStateConnector +import com.vitorpamplona.amethyst.model.torState.TorRelayState +import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager +import com.vitorpamplona.amethyst.service.crashreports.CrashReportCache +import com.vitorpamplona.amethyst.service.crashreports.UnexpectedCrashSaver +import com.vitorpamplona.amethyst.service.eventCache.MemoryTrimmingService +import com.vitorpamplona.amethyst.service.images.ImageCacheFactory +import com.vitorpamplona.amethyst.service.images.ImageLoaderSetup +import com.vitorpamplona.amethyst.service.location.LocationState +import com.vitorpamplona.amethyst.service.notifications.PokeyReceiver +import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager +import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache +import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket +import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache +import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCacheFactory +import com.vitorpamplona.amethyst.service.relayClient.CacheClientConnector +import com.vitorpamplona.amethyst.service.relayClient.RelayProxyClientConnector +import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.AuthCoordinator +import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.NotifyCoordinator +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator +import com.vitorpamplona.amethyst.service.relayClient.speedLogger.RelaySpeedLogger +import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory +import com.vitorpamplona.amethyst.ui.screen.UiSettingsState +import com.vitorpamplona.amethyst.ui.tor.TorManager +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache +import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OkHttpOtsResolverBuilder +import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OtsBlockHeightCache +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import java.io.File +import kotlin.run + +class AppModules( + val appContext: Context, +) { + val appAgent = "Amethyst/${BuildConfig.VERSION_NAME}" + + // Exists to avoid exceptions stopping the coroutine + val exceptionHandler = + CoroutineExceptionHandler { _, throwable -> + Log.e("AmethystCoroutine", "Caught exception: ${throwable.message}", throwable) + } + + val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler) + val applicationDefaultScope = CoroutineScope(Dispatchers.Default + SupervisorJob() + exceptionHandler) + + // Blocking load of UI Preferences to avoid theme/language blinking + val uiPrefs by lazy { + UiSharedPreferences(appContext, applicationIOScope) + } + + // Blocking load of Tor Settings to avoid connection leaks + val torPrefs by lazy { + TorSharedPreferences(appContext, applicationIOScope) + } + + // App services that should be run as soon as there are subscribers to their flows + val locationManager = LocationState(appContext, applicationIOScope) + val connManager = ConnectivityManager(appContext, applicationIOScope) + + val uiState by lazy { + UiSettingsState(uiPrefs.value, connManager.isMobileOrFalse, applicationIOScope) + } + + val torManager = TorManager(torPrefs, appContext, applicationIOScope) + + // Service that will run at all times to receive events from Pokey + val pokeyReceiver = PokeyReceiver() + + // Key cache service to download and decrypt encrypted files before caching them. + val keyCache = EncryptionKeyCache() + + // manages all the other connections separately from relays. + val okHttpClients = + DualHttpClientManager( + userAgent = appAgent, + proxyPortProvider = torManager.activePortOrNull, + isMobileDataProvider = connManager.isMobileOrNull, + keyCache = keyCache, + scope = applicationIOScope, + ) + + // manages all relay connections + val okHttpClientForRelays = + DualHttpClientManager( + userAgent = appAgent, + proxyPortProvider = torManager.activePortOrNull, + isMobileDataProvider = connManager.isMobileOrNull, + keyCache = keyCache, + scope = applicationIOScope, + ) + + // manages all relay connections + val okHttpClientForRelaysForDms = + DualHttpClientManager( + userAgent = appAgent, + proxyPortProvider = torManager.activePortOrNull, + isMobileDataProvider = connManager.isMobileOrNull, + keyCache = keyCache, + scope = applicationIOScope, + ) + + // Offers easy methods to know when connections are happening through Tor or not + val roleBasedHttpClientBuilder = RoleBasedHttpClientBuilder(okHttpClients, torPrefs.value) + + // Application-wide block height request cache + val otsBlockHeightCache by lazy { OtsBlockHeightCache() } + + val otsResolverBuilder: OkHttpOtsResolverBuilder = + OkHttpOtsResolverBuilder( + roleBasedHttpClientBuilder::okHttpClientForMoney, + roleBasedHttpClientBuilder::shouldUseTorForMoneyOperations, + otsBlockHeightCache, + ) + + // Application-wide ots verification cache + val otsVerifCache by lazy { VerificationStateCache(otsResolverBuilder) } + + val torEvaluatorFlow = + TorRelayState( + okHttpClients, + torPrefs.value, + applicationDefaultScope, + ) + + // Connects the NostrClient class with okHttp + val websocketBuilder = + OkHttpWebSocket.Builder { url -> + val useTor = torEvaluatorFlow.flow.value.useTor(url) + if (url in torEvaluatorFlow.flow.value.dmRelayList) { + okHttpClientForRelaysForDms.getHttpClient(useTor) + } else { + okHttpClientForRelays.getHttpClient(useTor) + } + } + + // Caches all events in Memory + val cache: LocalCache = LocalCache + + // Organizes cache clearing + val trimmingService = MemoryTrimmingService(cache) + + // Provides a relay pool + val client: INostrClient = NostrClient(websocketBuilder, applicationDefaultScope) + + // Watches for changes on Tor and Relay List Settings + val relayProxyClientConnector = RelayProxyClientConnector(torEvaluatorFlow.flow, okHttpClients, connManager, client, torManager, applicationDefaultScope) + + // Verifies and inserts in the cache from all relays, all subscriptions + val cacheClientConnector = CacheClientConnector(client, cache) + + // Show messages from the Relay and controls their dismissal + val notifyCoordinator = NotifyCoordinator(client) + + // Authenticates with relays. + val authCoordinator = AuthCoordinator(client, applicationDefaultScope) + + // Tries to verify new OTS events when they arrive. + val otsEventVerifier = IncomingOtsEventVerifier(otsVerifCache, cache, applicationDefaultScope) + + val logger = if (isDebug) RelaySpeedLogger(client) else null + + // Coordinates all subscriptions for the Nostr Client + val sources: RelaySubscriptionsCoordinator = RelaySubscriptionsCoordinator(LocalCache, client, applicationDefaultScope) + + // keeps all accounts live + val accountsCache = + AccountCacheState( + geolocationFlow = locationManager.geohashStateFlow, + nwcFilterAssembler = sources.nwc, + contentResolverFn = ::contentResolverFn, + otsResolverBuilder = otsResolverBuilder, + cache = cache, + client = client, + ) + + // as new accounts are loaded, updates the state of the TorRelaySettings, which produces new TorRelayEvaluator + // and reconnects relays if the configuration has been changed. + val accountsTorStateConnector = AccountsTorStateConnector(accountsCache, torEvaluatorFlow, applicationDefaultScope) + + // saves the .content of NIP-95 blobs in disk to save memory + val nip95cache: File by lazy { Nip95CacheFactory.new(appContext) } + + // local video cache with disk + memory + val videoCache: VideoCache by lazy { VideoCacheFactory.new(appContext) } + + // image cache in disk for coil + val diskCache: DiskCache by lazy { ImageCacheFactory.newDisk(appContext) } + + // image cache in memory for coil + val memoryCache: MemoryCache by lazy { ImageCacheFactory.newMemory(appContext) } + + // crash report storage + val crashReportCache: CrashReportCache by lazy { CrashReportCache(appContext) } + + // cache for NIP-11 documents + val nip11Cache: Nip11CachedRetriever by lazy { + Nip11CachedRetriever(torEvaluatorFlow::okHttpClientForRelay) + } + + fun contentResolverFn(): ContentResolver = appContext.contentResolver + + fun setImageLoader() { + ImageLoaderSetup.setup(appContext, diskCache, memoryCache) { url -> + okHttpClients.getHttpClient(roleBasedHttpClientBuilder.shouldUseTorForImageDownload(url)) + } + } + + fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences = EncryptedStorage.preferences(appContext, npub) + + fun initiate(appContext: Context) { + Thread.setDefaultUncaughtExceptionHandler(UnexpectedCrashSaver(crashReportCache, applicationIOScope)) + + // forces initialization of uiPrefs in the main thread to avoid blinking themes + uiPrefs + + // initializes diskcache on an IO thread. + applicationIOScope.launch { + // preloads tor preferences + torPrefs + + // Sets Coil - Tor - OkHttp link + setImageLoader() + + // prepares coil's disk cache + diskCache + + // prepares exoplayer's disk cache + videoCache + } + + // registers to receive events + pokeyReceiver.register(appContext) + } + + fun terminate(appContext: Context) { + pokeyReceiver.unregister(appContext) + applicationIOScope.cancel("Application onTerminate $appContext") + applicationDefaultScope.cancel("Application onTerminate $appContext") + accountsCache.clear() + } + + fun trim() { + applicationDefaultScope.launch { + trimmingService.run(null, LocalPreferences.allSavedAccounts()) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index dae1ecad9..4165bc395 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -30,12 +30,10 @@ import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.amethyst.model.ALL_FOLLOWS import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS -import com.vitorpamplona.amethyst.model.Settings +import com.vitorpamplona.amethyst.model.UiSettings import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName -import com.vitorpamplona.amethyst.ui.tor.TorSettings -import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -210,7 +208,7 @@ object LocalPreferences { } private val prefsDirPath: String - get() = "${Amethyst.instance.filesDir.parent}/shared_prefs/" + get() = "${Amethyst.instance.appContext.filesDir.parent}/shared_prefs/" private suspend fun addAccount(accInfo: AccountInfo) { val accounts = savedAccounts().filter { it.npub != accInfo.npub }.plus(accInfo) @@ -255,7 +253,7 @@ object LocalPreferences { return if (BuildConfig.DEBUG && DEBUG_PLAINTEXT_PREFERENCES) { val preferenceFile = if (npub == null) DEBUG_PREFERENCES_NAME else "${DEBUG_PREFERENCES_NAME}_$npub" - Amethyst.instance.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE) + Amethyst.instance.appContext.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE) } else { Amethyst.instance.encryptedStorage(npub) } @@ -475,8 +473,6 @@ object LocalPreferences { remove(PrefKeys.USE_PROXY) remove(PrefKeys.PROXY_PORT) - putString(PrefKeys.TOR_SETTINGS, JsonMapper.mapper.writeValueAsString(settings.torSettings.toSettings())) - val regularMap = settings.lastReadPerRoute.value.mapValues { it.value.value @@ -501,7 +497,7 @@ object LocalPreferences { suspend fun loadAccountConfigFromEncryptedStorage(): AccountSettings? = currentAccount()?.let { loadAccountConfigFromEncryptedStorage(it) } suspend fun saveSharedSettings( - sharedSettings: Settings, + sharedSettings: UiSettings, prefs: SharedPreferences = encryptedPreferences(), ) { Log.d("LocalPreferences", "Saving to shared settings") @@ -510,11 +506,13 @@ object LocalPreferences { } } - suspend fun loadSharedSettings(prefs: SharedPreferences = encryptedPreferences()): Settings? { + suspend fun loadSharedSettings(prefs: SharedPreferences = encryptedPreferences()): UiSettings? { Log.d("LocalPreferences", "Load shared settings") with(prefs) { return try { - getString(PrefKeys.SHARED_SETTINGS, "{}")?.let { JsonMapper.mapper.readValue(it) } + getString(PrefKeys.SHARED_SETTINGS, "{}")?.let { + JsonMapper.mapper.readValue(it) + } } catch (e: Throwable) { if (e is CancellationException) throw e Log.w( @@ -598,8 +596,6 @@ object LocalPreferences { val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false) val hideNIP17WarningDialog = getBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, false) - val torSettings = parseOrNull(PrefKeys.TOR_SETTINGS) ?: TorSettings() - val lastReadPerRoute = parseOrNull>(PrefKeys.LAST_READ_PER_ROUTE)?.mapValues { MutableStateFlow(it.value) @@ -637,7 +633,6 @@ object LocalPreferences { backupHashtagList = latestHashtagList, backupGeohashList = latestGeohashList, backupEphemeralChatList = latestEphemeralList, - torSettings = TorSettingsFlow.build(torSettings), lastReadPerRoute = MutableStateFlow(lastReadPerRoute), hasDonatedInVersion = MutableStateFlow(hasDonatedInVersion), pendingAttestations = MutableStateFlow(pendingAttestations), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 307a88ed5..6a49bfb00 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.model import android.util.Log import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.commons.richtext.RichTextParser @@ -80,7 +79,6 @@ import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState import com.vitorpamplona.amethyst.model.nip96FileStorage.FileStorageServerListState import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState -import com.vitorpamplona.amethyst.model.privacyOptions.PrivacyState import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineRelayListsState import com.vitorpamplona.amethyst.model.serverList.MergedServerListState @@ -89,7 +87,6 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.FeedDecryptionCaches import com.vitorpamplona.amethyst.model.topNavFeeds.FeedTopNavFilterState import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxLoaderState -import com.vitorpamplona.amethyst.model.torState.TorRelayState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler import com.vitorpamplona.amethyst.service.uploads.FileHeader @@ -136,8 +133,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds import com.vitorpamplona.quartz.nip01Core.tags.references.references -import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache -import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OkHttpOtsResolverBuilder +import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder import com.vitorpamplona.quartz.nip04Dm.PrivateDMCache import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent @@ -226,7 +222,7 @@ class Account( val signer: NostrSigner, val geolocationFlow: StateFlow, val nwcFilterAssembler: NWCPaymentFilterAssembler, - val otsVerifCache: VerificationStateCache, + val otsResolverBuilder: OtsResolverBuilder, val cache: LocalCache, val client: INostrClient, val scope: CoroutineScope, @@ -323,17 +319,6 @@ class Account( val chatroomList = cache.getOrCreateChatroomList(signer.pubKey) - val privacyState = PrivacyState(settings) - val torRelayState = TorRelayState(trustedRelays, dmRelayList, settings, scope) - - val otsResolverBuilder: OkHttpOtsResolverBuilder = - OkHttpOtsResolverBuilder( - { - Amethyst.instance.okHttpClients.getHttpClient(privacyState.shouldUseTorForMoneyOperations(it)) - }, - privacyState::shouldUseTorForMoneyOperations, - Amethyst.instance.otsBlockHeightCache, - ) val newNotesPreProcessor = EventProcessor(this, cache) val otsState = OtsState(signer, cache, otsResolverBuilder, scope, settings) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index 92fee1c86..3c3268fd7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -24,8 +24,6 @@ import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.screen.FeedDefinition -import com.vitorpamplona.amethyst.ui.tor.TorSettings -import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair @@ -132,7 +130,6 @@ class AccountSettings( var backupHashtagList: HashtagListEvent? = null, var backupGeohashList: GeohashListEvent? = null, var backupEphemeralChatList: EphemeralChatListEvent? = null, - val torSettings: TorSettingsFlow = TorSettingsFlow(), val lastReadPerRoute: MutableStateFlow>> = MutableStateFlow(mapOf()), var hasDonatedInVersion: MutableStateFlow> = MutableStateFlow(setOf()), val pendingAttestations: MutableStateFlow> = MutableStateFlow>(mapOf()), @@ -249,17 +246,6 @@ class AccountSettings( } } - // --- - // proxy settings - // --- - fun setTorSettings(newTorSettings: TorSettings): Boolean = - if (torSettings.update(newTorSettings)) { - saveAccountSettings() - true - } else { - false - } - // --- // language services // --- diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt index 356f79707..3a1b5c1a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.R @Stable -data class Settings( +data class UiSettings( val theme: ThemeType = ThemeType.SYSTEM, val preferredLanguage: String? = null, val automaticallyShowImages: ConnectivityType = ConnectivityType.ALWAYS, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt new file mode 100644 index 000000000..e51f89b14 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt @@ -0,0 +1,165 @@ +/** + * 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.model + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine + +class UiSettingsFlow( + val theme: MutableStateFlow = MutableStateFlow(ThemeType.SYSTEM), + val preferredLanguage: MutableStateFlow = MutableStateFlow(null), + val automaticallyShowImages: MutableStateFlow = MutableStateFlow(ConnectivityType.ALWAYS), + val automaticallyStartPlayback: MutableStateFlow = MutableStateFlow(ConnectivityType.ALWAYS), + val automaticallyShowUrlPreview: MutableStateFlow = MutableStateFlow(ConnectivityType.ALWAYS), + val automaticallyHideNavigationBars: MutableStateFlow = MutableStateFlow(BooleanType.ALWAYS), + val automaticallyShowProfilePictures: MutableStateFlow = MutableStateFlow(ConnectivityType.ALWAYS), + val dontShowPushNotificationSelector: MutableStateFlow = MutableStateFlow(false), + val dontAskForNotificationPermissions: MutableStateFlow = MutableStateFlow(false), + val featureSet: MutableStateFlow = MutableStateFlow(FeatureSetType.SIMPLIFIED), + val gallerySet: MutableStateFlow = MutableStateFlow(ProfileGalleryType.CLASSIC), +) { + // emits at every change in any of the propertyes. + val propertyWatchFlow = + combine( + listOf( + theme, + preferredLanguage, + automaticallyShowImages, + automaticallyStartPlayback, + automaticallyShowUrlPreview, + automaticallyHideNavigationBars, + automaticallyShowProfilePictures, + dontShowPushNotificationSelector, + dontAskForNotificationPermissions, + featureSet, + gallerySet, + ), + ) { flows -> + UiSettings( + flows[0] as ThemeType, + flows[1] as String?, + flows[2] as ConnectivityType, + flows[3] as ConnectivityType, + flows[4] as ConnectivityType, + flows[5] as BooleanType, + flows[6] as ConnectivityType, + flows[7] as Boolean, + flows[8] as Boolean, + flows[9] as FeatureSetType, + flows[10] as ProfileGalleryType, + ) + } + + fun toSettings(): UiSettings = + UiSettings( + theme.value, + preferredLanguage.value, + automaticallyShowImages.value, + automaticallyStartPlayback.value, + automaticallyShowUrlPreview.value, + automaticallyHideNavigationBars.value, + automaticallyShowProfilePictures.value, + dontShowPushNotificationSelector.value, + dontAskForNotificationPermissions.value, + featureSet.value, + gallerySet.value, + ) + + fun update(torSettings: UiSettings): Boolean { + var any = false + + if (theme.value != torSettings.theme) { + theme.tryEmit(torSettings.theme) + any = true + } + if (preferredLanguage.value != torSettings.preferredLanguage) { + preferredLanguage.tryEmit(torSettings.preferredLanguage) + any = true + } + if (automaticallyShowImages.value != torSettings.automaticallyShowImages) { + automaticallyShowImages.tryEmit(torSettings.automaticallyShowImages) + any = true + } + if (automaticallyStartPlayback.value != torSettings.automaticallyStartPlayback) { + automaticallyStartPlayback.tryEmit(torSettings.automaticallyStartPlayback) + any = true + } + if (automaticallyShowUrlPreview.value != torSettings.automaticallyShowUrlPreview) { + automaticallyShowUrlPreview.tryEmit(torSettings.automaticallyShowUrlPreview) + any = true + } + if (automaticallyHideNavigationBars.value != torSettings.automaticallyHideNavigationBars) { + automaticallyHideNavigationBars.tryEmit(torSettings.automaticallyHideNavigationBars) + any = true + } + if (automaticallyShowProfilePictures.value != torSettings.automaticallyShowProfilePictures) { + automaticallyShowProfilePictures.tryEmit(torSettings.automaticallyShowProfilePictures) + any = true + } + if (dontShowPushNotificationSelector.value != torSettings.dontShowPushNotificationSelector) { + dontShowPushNotificationSelector.tryEmit(torSettings.dontShowPushNotificationSelector) + any = true + } + if (dontAskForNotificationPermissions.value != torSettings.dontAskForNotificationPermissions) { + dontAskForNotificationPermissions.tryEmit(torSettings.dontAskForNotificationPermissions) + any = true + } + if (featureSet.value != torSettings.featureSet) { + featureSet.tryEmit(torSettings.featureSet) + any = true + } + if (gallerySet.value != torSettings.gallerySet) { + gallerySet.tryEmit(torSettings.gallerySet) + any = true + } + + return any + } + + fun dontShowPushNotificationSelector() { + if (!dontShowPushNotificationSelector.value) { + dontShowPushNotificationSelector.tryEmit(true) + } + } + + fun dontAskForNotificationPermissions() { + if (!dontAskForNotificationPermissions.value) { + dontAskForNotificationPermissions.tryEmit(true) + } + } + + companion object { + fun build(torSettings: UiSettings): UiSettingsFlow = + UiSettingsFlow( + MutableStateFlow(torSettings.theme), + MutableStateFlow(torSettings.preferredLanguage), + MutableStateFlow(torSettings.automaticallyShowImages), + MutableStateFlow(torSettings.automaticallyStartPlayback), + MutableStateFlow(torSettings.automaticallyShowUrlPreview), + MutableStateFlow(torSettings.automaticallyHideNavigationBars), + MutableStateFlow(torSettings.automaticallyShowProfilePictures), + MutableStateFlow(torSettings.dontShowPushNotificationSelector), + MutableStateFlow(torSettings.dontAskForNotificationPermissions), + MutableStateFlow(torSettings.featureSet), + MutableStateFlow(torSettings.gallerySet), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt index c23d60820..700e5fdc3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt @@ -32,7 +32,7 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal -import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache +import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope @@ -47,6 +47,7 @@ class AccountCacheState( val geolocationFlow: StateFlow, val nwcFilterAssembler: NWCPaymentFilterAssembler, val contentResolverFn: () -> ContentResolver, + val otsResolverBuilder: OtsResolverBuilder, val cache: LocalCache, val client: INostrClient, ) { @@ -91,7 +92,7 @@ class AccountCacheState( signer = signer, geolocationFlow = geolocationFlow, nwcFilterAssembler = nwcFilterAssembler, - otsVerifCache = VerificationStateCache(), + otsResolverBuilder = otsResolverBuilder, cache = cache, client = client, scope = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/LoadRelayInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/LoadRelayInfo.kt new file mode 100644 index 000000000..94aba84e2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/LoadRelayInfo.kt @@ -0,0 +1,52 @@ +/** + * 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.model.nip11RelayInfo + +import android.util.Log +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.produceState +import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation + +@Composable +fun loadRelayInfo(relay: NormalizedRelayUrl): State = loadRelayInfo(relay, Amethyst.instance.nip11Cache) + +@Composable +fun loadRelayInfo( + relay: NormalizedRelayUrl, + nip11Cache: Nip11CachedRetriever, +): State = + produceState( + nip11Cache.getFromCache(relay), + relay, + ) { + nip11Cache.loadRelayInfo( + relay = relay, + onInfo = { + value = it + }, + onError = { url, errorCode, exceptionMessage -> + Log.e("RelayInfo", "Error loading relay info for ${relay.url}: $errorCode - $exceptionMessage") + }, + ) + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11CachedRetriever.kt similarity index 54% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11CachedRetriever.kt index ed6eb24ef..dbd59899f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11CachedRetriever.kt @@ -18,51 +18,21 @@ * 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.service +package com.vitorpamplona.amethyst.model.nip11RelayInfo -import android.util.Log import android.util.LruCache import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation -import com.vitorpamplona.quartz.utils.TimeUtils -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.coroutines.executeAsync - -object Nip11CachedRetriever { - sealed class RetrieveResult( - val data: Nip11RelayInformation, - val time: Long, - ) { - class Error( - data: Nip11RelayInformation, - val error: Nip11Retriever.ErrorCode, - val msg: String? = null, - ) : RetrieveResult(data, TimeUtils.now()) - - class Success( - data: Nip11RelayInformation, - ) : RetrieveResult(data, TimeUtils.now()) - - class Loading( - data: Nip11RelayInformation, - ) : RetrieveResult(data, TimeUtils.now()) - - class Empty( - data: Nip11RelayInformation, - ) : RetrieveResult(data, TimeUtils.now()) - - fun isValid() = time > TimeUtils.oneHourAgo() - } +class Nip11CachedRetriever( + val okHttpClient: (NormalizedRelayUrl) -> OkHttpClient, +) { private val relayInformationEmptyCache = LruCache(1000) private val relayInformationDocumentCache = LruCache(1000) - private val retriever = Nip11Retriever() + private val retriever = Nip11Retriever(okHttpClient) fun getEmpty(relay: NormalizedRelayUrl): Nip11RelayInformation { relayInformationEmptyCache.get(relay)?.let { return it } @@ -98,7 +68,6 @@ object Nip11CachedRetriever { suspend fun loadRelayInfo( relay: NormalizedRelayUrl, - okHttpClient: (String) -> OkHttpClient, onInfo: (Nip11RelayInformation) -> Unit, onError: (NormalizedRelayUrl, Nip11Retriever.ErrorCode, String?) -> Unit, ) { @@ -110,33 +79,31 @@ object Nip11CachedRetriever { if (doc.isValid()) { // just wait. } else { - retrieve(relay, okHttpClient, onInfo, onError) + retrieve(relay, onInfo, onError) } } is RetrieveResult.Error -> { if (doc.isValid()) { onError(relay, doc.error, null) } else { - retrieve(relay, okHttpClient, onInfo, onError) + retrieve(relay, onInfo, onError) } } - is RetrieveResult.Empty -> retrieve(relay, okHttpClient, onInfo, onError) + is RetrieveResult.Empty -> retrieve(relay, onInfo, onError) } } else { - retrieve(relay, okHttpClient, onInfo, onError) + retrieve(relay, onInfo, onError) } } private suspend fun retrieve( relay: NormalizedRelayUrl, - okHttpClient: (String) -> OkHttpClient, onInfo: (Nip11RelayInformation) -> Unit, onError: (NormalizedRelayUrl, Nip11Retriever.ErrorCode, String?) -> Unit, ) { relayInformationDocumentCache.put(relay, RetrieveResult.Loading(getEmpty(relay))) retriever.loadRelayInfo( relay = relay, - okHttpClient = okHttpClient, onInfo = { relayInformationDocumentCache.put(relay, RetrieveResult.Success(it)) relayInformationEmptyCache.remove(relay) @@ -150,60 +117,3 @@ object Nip11CachedRetriever { ) } } - -class Nip11Retriever { - enum class ErrorCode { - FAIL_TO_ASSEMBLE_URL, - FAIL_TO_REACH_SERVER, - FAIL_TO_PARSE_RESULT, - FAIL_WITH_HTTP_STATUS, - } - - suspend fun loadRelayInfo( - relay: NormalizedRelayUrl, - okHttpClient: (String) -> OkHttpClient, - onInfo: (Nip11RelayInformation) -> Unit, - onError: (NormalizedRelayUrl, ErrorCode, String?) -> Unit, - ) { - val url = relay.toHttp() - try { - val request: Request = - Request - .Builder() - .header("Accept", "application/nostr+json") - .url(url) - .build() - - val client = okHttpClient(url) - - client.newCall(request).executeAsync().use { response -> - withContext(Dispatchers.IO) { - val body = response.body.string() - try { - if (response.isSuccessful) { - if (body.startsWith("{")) { - onInfo(Nip11RelayInformation.fromJson(body)) - } else { - onError(relay, ErrorCode.FAIL_TO_PARSE_RESULT, body) - } - } else { - onError(relay, ErrorCode.FAIL_WITH_HTTP_STATUS, response.code.toString()) - } - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.e( - "RelayInfoFail", - "Resulting Message from Relay ${relay.url} in not parseable: $body", - e, - ) - onError(relay, ErrorCode.FAIL_TO_PARSE_RESULT, e.message) - } - } - } - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.e("RelayInfoFail", "Invalid URL ${relay.url}", e) - onError(relay, ErrorCode.FAIL_TO_ASSEMBLE_URL, e.message) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11Retriever.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11Retriever.kt new file mode 100644 index 000000000..04892f908 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/Nip11Retriever.kt @@ -0,0 +1,90 @@ +/** + * 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.model.nip11RelayInfo + +import android.util.Log +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp +import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.coroutines.executeAsync + +class Nip11Retriever( + val okHttpClient: (NormalizedRelayUrl) -> OkHttpClient, +) { + enum class ErrorCode { + FAIL_TO_ASSEMBLE_URL, + FAIL_TO_REACH_SERVER, + FAIL_TO_PARSE_RESULT, + FAIL_WITH_HTTP_STATUS, + } + + suspend fun loadRelayInfo( + relay: NormalizedRelayUrl, + onInfo: (Nip11RelayInformation) -> Unit, + onError: (NormalizedRelayUrl, ErrorCode, String?) -> Unit, + ) { + val url = relay.toHttp() + try { + val request: Request = + Request + .Builder() + .header("Accept", "application/nostr+json") + .url(url) + .build() + + val client = okHttpClient(relay) + + client.newCall(request).executeAsync().use { response -> + withContext(Dispatchers.IO) { + val body = response.body.string() + try { + if (response.isSuccessful) { + if (body.startsWith("{")) { + onInfo(Nip11RelayInformation.fromJson(body)) + } else { + onError(relay, ErrorCode.FAIL_TO_PARSE_RESULT, body) + } + } else { + onError(relay, ErrorCode.FAIL_WITH_HTTP_STATUS, response.code.toString()) + } + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e( + "RelayInfoFail", + "Resulting Message from Relay ${relay.url} in not parseable: $body", + e, + ) + onError(relay, ErrorCode.FAIL_TO_PARSE_RESULT, e.message) + } + } + } + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("RelayInfoFail", "Invalid URL ${relay.url}", e) + onError(relay, ErrorCode.FAIL_TO_ASSEMBLE_URL, e.message) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/RetrieveResult.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/RetrieveResult.kt new file mode 100644 index 000000000..bdc158317 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip11RelayInfo/RetrieveResult.kt @@ -0,0 +1,49 @@ +/** + * 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.model.nip11RelayInfo + +import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation +import com.vitorpamplona.quartz.utils.TimeUtils + +sealed class RetrieveResult( + val data: Nip11RelayInformation, + val time: Long, +) { + class Error( + data: Nip11RelayInformation, + val error: Nip11Retriever.ErrorCode, + val msg: String? = null, + ) : RetrieveResult(data, TimeUtils.now()) + + class Success( + data: Nip11RelayInformation, + ) : RetrieveResult(data, TimeUtils.now()) + + class Loading( + data: Nip11RelayInformation, + ) : RetrieveResult(data, TimeUtils.now()) + + class Empty( + data: Nip11RelayInformation, + ) : RetrieveResult(data, TimeUtils.now()) + + fun isValid() = time > TimeUtils.oneHourAgo() +} 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 new file mode 100644 index 000000000..f4fcf34d1 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt @@ -0,0 +1,184 @@ +/** + * 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.model.preferences + +import android.content.Context +import android.util.Log +import androidx.appcompat.app.AppCompatDelegate +import androidx.compose.runtime.Stable +import androidx.core.os.LocaleListCompat +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.fasterxml.jackson.module.kotlin.readValue +import com.vitorpamplona.amethyst.LocalPreferences +import com.vitorpamplona.amethyst.model.UiSettings +import com.vitorpamplona.amethyst.model.UiSettingsFlow +import com.vitorpamplona.amethyst.ui.tor.TorSettings +import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.runBlocking + +val Context.sharedPreferencesDataStore: DataStore by preferencesDataStore(name = "shared_settings") + +@Stable +class UiSharedPreferences( + val context: Context, + val scope: CoroutineScope, +) { + companion object { + val UI_SETTINGS = stringPreferencesKey("ui_settings") + } + + // UI Preferences. Makes sure to wait for it to avoid blinking themes and language preferences + val value = + runBlocking { + UiSettingsFlow.build(uiPreferences() ?: UiSettings()) + } + + val languageUpdate = + value.preferredLanguage + .onEach { language -> + AppCompatDelegate.setApplicationLocales( + LocaleListCompat.forLanguageTags(language), + ) + }.flowOn(Dispatchers.Main) + .stateIn( + scope, + SharingStarted.Eagerly, + value.toSettings(), + ) + + @OptIn(FlowPreview::class) + val saving = + value.propertyWatchFlow + .debounce(1000) + .distinctUntilChanged() + .onEach(::save) + .flowOn(Dispatchers.IO) + .stateIn( + scope, + SharingStarted.Eagerly, + value.toSettings(), + ) + + suspend fun uiPreferences(): UiSettings? = + try { + // Get the preference flow and take the first value. + val preferences = context.sharedPreferencesDataStore.data.first() + val newVersion = preferences[UI_SETTINGS]?.let { JsonMapper.mapper.readValue(it) } + + if (newVersion != null) { + newVersion + } else { + val oldVersion = LocalPreferences.loadSharedSettings() + if (oldVersion != null) { + save(oldVersion) + } + oldVersion + } + } catch (e: Exception) { + // Log any errors that occur while reading the DataStore. + Log.e("SharedPreferences", "Error reading DataStore preferences: ${e.message}") + null + } + + suspend fun save(sharedSettings: UiSettings) { + try { + val str = JsonMapper.mapper.writeValueAsString(sharedSettings) + context.sharedPreferencesDataStore.edit { preferences -> + preferences[UI_SETTINGS] = str + } + } catch (e: Exception) { + // Log any errors that occur while reading the DataStore. + Log.e("SharedPreferences", "Error saving DataStore preferences: ${e.message}") + } + } +} + +@Stable +class TorSharedPreferences( + val context: Context, + val scope: CoroutineScope, +) { + companion object { + val TOR_SETTINGS = stringPreferencesKey("tor_settings") + } + + // Tor Preferences. Makes sure to wait for it to avoid connecting with random IPs + val value = + runBlocking { + TorSettingsFlow.build(torPreferences() ?: TorSettings()) + } + + @OptIn(FlowPreview::class) + val saving = + value.propertyWatchFlow + .debounce(1000) + .distinctUntilChanged() + .onEach(::save) + .flowOn(Dispatchers.IO) + .stateIn( + scope, + SharingStarted.Eagerly, + value.toSettings(), + ) + + suspend fun torPreferences(): TorSettings? = + try { + // Get the preference flow and take the first value. + val preferences = context.sharedPreferencesDataStore.data.first() + println("AABBCC Loading TorSettings: ${preferences[TOR_SETTINGS]}") + preferences[TOR_SETTINGS]?.let { + println("AABBCC Loading TorSettings: $it") + JsonMapper.mapper.readValue(it) + } + } catch (e: Exception) { + // Log any errors that occur while reading the DataStore. + Log.e("SharedPreferences", "Error reading DataStore preferences: ${e.message}") + null + } + + suspend fun save(torSettings: TorSettings) { + try { + println("AABBCC Saving TorSettings: $torSettings") + val str = JsonMapper.mapper.writeValueAsString(torSettings) + context.sharedPreferencesDataStore.edit { preferences -> + preferences[TOR_SETTINGS] = str + } + } catch (e: Exception) { + // Log any errors that occur while reading the DataStore. + Log.e("SharedPreferences", "Error saving DataStore preferences: ${e.message}") + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/EmptyRoleBasedHttpClientBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/EmptyRoleBasedHttpClientBuilder.kt new file mode 100644 index 000000000..982083af5 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/EmptyRoleBasedHttpClientBuilder.kt @@ -0,0 +1,50 @@ +/** + * 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.model.privacyOptions + +import okhttp3.OkHttpClient +import java.net.InetSocketAddress + +class EmptyRoleBasedHttpClientBuilder : IRoleBasedHttpClientBuilder { + val rootOkHttpClient by lazy { + OkHttpClient + .Builder() + .followRedirects(true) + .followSslRedirects(true) + .build() + } + + override fun proxyPortForVideo(url: String) = (rootOkHttpClient.proxy?.address() as? InetSocketAddress)?.port + + override fun okHttpClientForNip05(url: String) = rootOkHttpClient + + override fun okHttpClientForUploads(url: String) = rootOkHttpClient + + override fun okHttpClientForImage(url: String) = rootOkHttpClient + + override fun okHttpClientForVideo(url: String) = rootOkHttpClient + + override fun okHttpClientForMoney(url: String) = rootOkHttpClient + + override fun okHttpClientForPreview(url: String) = rootOkHttpClient + + override fun okHttpClientForPushRegistration(url: String) = rootOkHttpClient +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/IRoleBasedHttpClientBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/IRoleBasedHttpClientBuilder.kt new file mode 100644 index 000000000..0b75bb3d6 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/IRoleBasedHttpClientBuilder.kt @@ -0,0 +1,41 @@ +/** + * 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.model.privacyOptions + +import okhttp3.OkHttpClient + +interface IRoleBasedHttpClientBuilder { + fun proxyPortForVideo(url: String): Int? + + fun okHttpClientForNip05(url: String): OkHttpClient + + fun okHttpClientForUploads(url: String): OkHttpClient + + fun okHttpClientForImage(url: String): OkHttpClient + + fun okHttpClientForVideo(url: String): OkHttpClient + + fun okHttpClientForMoney(url: String): OkHttpClient + + fun okHttpClientForPreview(url: String): OkHttpClient + + fun okHttpClientForPushRegistration(url: String): OkHttpClient +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/PrivacyState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/RoleBasedHttpClientBuilder.kt similarity index 60% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/PrivacyState.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/RoleBasedHttpClientBuilder.kt index dcbee7e43..7d7f61ec6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/PrivacyState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/RoleBasedHttpClientBuilder.kt @@ -20,18 +20,21 @@ */ package com.vitorpamplona.amethyst.model.privacyOptions -import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager +import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import com.vitorpamplona.amethyst.ui.tor.TorType import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import okhttp3.OkHttpClient -class PrivacyState( - val settings: AccountSettings, -) { +class RoleBasedHttpClientBuilder( + val okHttpClient: DualHttpClientManager, + val torSettings: TorSettingsFlow, +) : IRoleBasedHttpClientBuilder { fun shouldUseTorForImageDownload(url: String) = shouldUseTorFor( url, - settings.torSettings.torType.value, - settings.torSettings.imagesViaTor.value, + torSettings.torType.value, + torSettings.imagesViaTor.value, ) fun shouldUseTorFor( @@ -57,37 +60,37 @@ class PrivacyState( } fun shouldUseTorForVideoDownload() = - when (settings.torSettings.torType.value) { + when (torSettings.torType.value) { TorType.OFF -> false - TorType.INTERNAL -> settings.torSettings.videosViaTor.value - TorType.EXTERNAL -> settings.torSettings.videosViaTor.value + TorType.INTERNAL -> torSettings.videosViaTor.value + TorType.EXTERNAL -> torSettings.videosViaTor.value } fun shouldUseTorForVideoDownload(url: String) = - when (settings.torSettings.torType.value) { + when (torSettings.torType.value) { TorType.OFF -> false - TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.videosViaTor.value) - TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.videosViaTor.value) + TorType.INTERNAL -> checkLocalHostOnionAndThen(url, torSettings.videosViaTor.value) + TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, torSettings.videosViaTor.value) } fun shouldUseTorForPreviewUrl(url: String) = - when (settings.torSettings.torType.value) { + when (torSettings.torType.value) { TorType.OFF -> false - TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.urlPreviewsViaTor.value) - TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.urlPreviewsViaTor.value) + TorType.INTERNAL -> checkLocalHostOnionAndThen(url, torSettings.urlPreviewsViaTor.value) + TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, torSettings.urlPreviewsViaTor.value) } fun shouldUseTorForTrustedRelays() = - when (settings.torSettings.torType.value) { + when (torSettings.torType.value) { TorType.OFF -> false - TorType.INTERNAL -> settings.torSettings.trustedRelaysViaTor.value - TorType.EXTERNAL -> settings.torSettings.trustedRelaysViaTor.value + TorType.INTERNAL -> torSettings.trustedRelaysViaTor.value + TorType.EXTERNAL -> torSettings.trustedRelaysViaTor.value } private fun checkLocalHostOnionAndThen( url: String, final: Boolean, - ): Boolean = checkLocalHostOnionAndThen(url, settings.torSettings.onionRelaysViaTor.value, final) + ): Boolean = checkLocalHostOnionAndThen(url, torSettings.onionRelaysViaTor.value, final) private fun checkLocalHostOnionAndThen( normalizedUrl: String, @@ -103,23 +106,39 @@ class PrivacyState( } fun shouldUseTorForMoneyOperations(url: String) = - when (settings.torSettings.torType.value) { + when (torSettings.torType.value) { TorType.OFF -> false - TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.moneyOperationsViaTor.value) - TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.moneyOperationsViaTor.value) + TorType.INTERNAL -> checkLocalHostOnionAndThen(url, torSettings.moneyOperationsViaTor.value) + TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, torSettings.moneyOperationsViaTor.value) } fun shouldUseTorForNIP05(url: String) = - when (settings.torSettings.torType.value) { + when (torSettings.torType.value) { TorType.OFF -> false - TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip05VerificationsViaTor.value) - TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip05VerificationsViaTor.value) + TorType.INTERNAL -> checkLocalHostOnionAndThen(url, torSettings.nip05VerificationsViaTor.value) + TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, torSettings.nip05VerificationsViaTor.value) } fun shouldUseTorForUploads(url: String) = - when (settings.torSettings.torType.value) { + when (torSettings.torType.value) { TorType.OFF -> false - TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip96UploadsViaTor.value) - TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip96UploadsViaTor.value) + TorType.INTERNAL -> checkLocalHostOnionAndThen(url, torSettings.nip96UploadsViaTor.value) + TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, torSettings.nip96UploadsViaTor.value) } + + override fun proxyPortForVideo(url: String): Int? = okHttpClient.getCurrentProxyPort(shouldUseTorForVideoDownload(url)) + + override fun okHttpClientForNip05(url: String): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForNIP05(url)) + + override fun okHttpClientForUploads(url: String): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForUploads(url)) + + override fun okHttpClientForImage(url: String): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForImageDownload(url)) + + override fun okHttpClientForVideo(url: String): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForVideoDownload(url)) + + override fun okHttpClientForMoney(url: String): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForMoneyOperations(url)) + + override fun okHttpClientForPreview(url: String): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForPreviewUrl(url)) + + override fun okHttpClientForPushRegistration(url: String): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForTrustedRelays()) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/SingleRoleBasedHttpClientBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/SingleRoleBasedHttpClientBuilder.kt new file mode 100644 index 000000000..6c3e85304 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/SingleRoleBasedHttpClientBuilder.kt @@ -0,0 +1,44 @@ +/** + * 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.model.privacyOptions + +import okhttp3.OkHttpClient +import java.net.InetSocketAddress + +class SingleRoleBasedHttpClientBuilder( + val okHttpClient: OkHttpClient, +) : IRoleBasedHttpClientBuilder { + override fun proxyPortForVideo(url: String) = (okHttpClient.proxy?.address() as? InetSocketAddress)?.port + + override fun okHttpClientForNip05(url: String) = okHttpClient + + override fun okHttpClientForUploads(url: String) = okHttpClient + + override fun okHttpClientForImage(url: String) = okHttpClient + + override fun okHttpClientForVideo(url: String) = okHttpClient + + override fun okHttpClientForMoney(url: String) = okHttpClient + + override fun okHttpClientForPreview(url: String) = okHttpClient + + override fun okHttpClientForPushRegistration(url: String) = okHttpClient +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt new file mode 100644 index 000000000..cea459d20 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt @@ -0,0 +1,101 @@ +/** + * 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.model.torState + +import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest +import kotlin.collections.ifEmpty + +class AccountsTorStateConnector( + accountsCache: AccountCacheState, + torEvaluatorFlow: TorRelayState, + scope: CoroutineScope, +) { + @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) + val allDmRelayFlows: Flow> = + accountsCache.accounts + .debounce(200) + .transformLatest { snapshot -> + val dmFlows = snapshot.map { it.value.dmRelayList.flow } + + val dmFlowReady = + dmFlows.ifEmpty { + listOf(MutableStateFlow(emptySet())) + } + + emitAll( + combine(dmFlowReady) { + val dmRelays = mutableSetOf() + it.forEach { + dmRelays.addAll(it) + } + dmRelays.toSet() + }, + ) + }.onEach { + torEvaluatorFlow.dmRelays.tryEmit(it) + }.stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) + + @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) + val allTrustedRelaysFlow: Flow> = + accountsCache.accounts + .debounce(200) + .transformLatest { snapshot -> + val dmFlows = snapshot.map { it.value.dmRelayList.flow } + + val dmFlowReady = + dmFlows.ifEmpty { + listOf(MutableStateFlow(emptySet())) + } + + emitAll( + combine(dmFlowReady) { + val dmRelays = mutableSetOf() + it.forEach { + dmRelays.addAll(it) + } + dmRelays.toSet() + }, + ) + }.onEach { + torEvaluatorFlow.trustedRelays.tryEmit(it) + }.stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt index f1190941e..6b0dfc6a4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt @@ -20,33 +20,36 @@ */ package com.vitorpamplona.amethyst.model.torState -import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState -import com.vitorpamplona.amethyst.model.serverList.TrustedRelayListsState +import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager +import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import com.vitorpamplona.amethyst.ui.tor.TorType import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn +import okhttp3.OkHttpClient class TorRelayState( - val trustedRelayState: TrustedRelayListsState, - val dmRelayState: DmRelayListState, - val settings: AccountSettings, + val okHttpClient: DualHttpClientManager, + val torSettingsFlow: TorSettingsFlow, val scope: CoroutineScope, ) { + val dmRelays = MutableStateFlow>(emptySet()) + val trustedRelays = MutableStateFlow>(emptySet()) + val torSettings = combine( - settings.torSettings.torType, - settings.torSettings.onionRelaysViaTor, - settings.torSettings.dmRelaysViaTor, - settings.torSettings.trustedRelaysViaTor, - settings.torSettings.newRelaysViaTor, + torSettingsFlow.torType, + torSettingsFlow.onionRelaysViaTor, + torSettingsFlow.dmRelaysViaTor, + torSettingsFlow.trustedRelaysViaTor, + torSettingsFlow.newRelaysViaTor, ) { torType: TorType, onionRelaysViaTor: Boolean, @@ -64,11 +67,11 @@ class TorRelayState( }.onStart { emit( TorRelaySettings( - torType = settings.torSettings.torType.value, - onionRelaysViaTor = settings.torSettings.onionRelaysViaTor.value, - dmRelaysViaTor = settings.torSettings.dmRelaysViaTor.value, - trustedRelaysViaTor = settings.torSettings.trustedRelaysViaTor.value, - newRelaysViaTor = settings.torSettings.newRelaysViaTor.value, + torType = torSettingsFlow.torType.value, + onionRelaysViaTor = torSettingsFlow.onionRelaysViaTor.value, + dmRelaysViaTor = torSettingsFlow.dmRelaysViaTor.value, + trustedRelaysViaTor = torSettingsFlow.trustedRelaysViaTor.value, + newRelaysViaTor = torSettingsFlow.newRelaysViaTor.value, ), ) }.flowOn(Dispatchers.Default) @@ -76,20 +79,21 @@ class TorRelayState( scope, SharingStarted.Eagerly, TorRelaySettings( - torType = settings.torSettings.torType.value, - onionRelaysViaTor = settings.torSettings.onionRelaysViaTor.value, - dmRelaysViaTor = settings.torSettings.dmRelaysViaTor.value, - trustedRelaysViaTor = settings.torSettings.trustedRelaysViaTor.value, - newRelaysViaTor = settings.torSettings.newRelaysViaTor.value, + torType = torSettingsFlow.torType.value, + onionRelaysViaTor = torSettingsFlow.onionRelaysViaTor.value, + dmRelaysViaTor = torSettingsFlow.dmRelaysViaTor.value, + trustedRelaysViaTor = torSettingsFlow.trustedRelaysViaTor.value, + newRelaysViaTor = torSettingsFlow.newRelaysViaTor.value, ), ) val flow = combineTransform( torSettings, - trustedRelayState.flow, - dmRelayState.flow, + trustedRelays, + dmRelays, ) { torSettings: TorRelaySettings, trustedRelayList: Set, dmRelayList: Set -> + println("AABBCC New tor evaluation: $torSettings $trustedRelayList $dmRelayList") emit( TorRelayEvaluation( torSettings = torSettings, @@ -101,8 +105,8 @@ class TorRelayState( emit( TorRelayEvaluation( torSettings = torSettings.value, - trustedRelayList = trustedRelayState.flow.value, - dmRelayList = dmRelayState.flow.value, + trustedRelayList = trustedRelays.value, + dmRelayList = dmRelays.value, ), ) }.flowOn(Dispatchers.Default) @@ -111,10 +115,12 @@ class TorRelayState( SharingStarted.Eagerly, TorRelayEvaluation( torSettings = torSettings.value, - trustedRelayList = trustedRelayState.flow.value, - dmRelayList = dmRelayState.flow.value, + trustedRelayList = trustedRelays.value, + dmRelayList = dmRelays.value, ), ) - fun shouldUseTorForClean(relay: NormalizedRelayUrl) = flow.value.useTor(relay) + fun shouldUseTorForRelay(relay: NormalizedRelayUrl) = flow.value.useTor(relay) + + fun okHttpClientForRelay(url: NormalizedRelayUrl): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForRelay(url)) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ApplicationExt.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ContextExt.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/ApplicationExt.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/service/ContextExt.kt index 13f7e5e04..946fea16f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ApplicationExt.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ContextExt.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.service -import android.app.Application +import android.content.Context import java.io.File -fun Application.safeCacheDir(): File { +fun Context.safeCacheDir(): File { val cacheDir = checkNotNull(cacheDir) { "cacheDir == null" } return cacheDir.apply { mkdirs() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityFlow.kt index 805e088d0..d80d8faa9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityFlow.kt @@ -39,7 +39,7 @@ class ConnectivityFlow( @OptIn(FlowPreview::class) val status = callbackFlow { - trySend(ConnectivityStatus.Connecting) + trySend(ConnectivityStatus.StartingService) val connectivityManager = context.getConnectivityManager() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityManager.kt index 5ec4e51ec..151e0eed9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityManager.kt @@ -41,7 +41,7 @@ class ConnectivityManager( ConnectivityFlow(context).status.distinctUntilChanged().stateIn( scope, SharingStarted.WhileSubscribed(30000), - ConnectivityStatus.Off, + ConnectivityStatus.StartingService, ) val isMobileOrNull: StateFlow = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityStatus.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityStatus.kt index 998d6e859..1276c411e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityStatus.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityStatus.kt @@ -28,5 +28,5 @@ sealed class ConnectivityStatus { object Off : ConnectivityStatus() - object Connecting : ConnectivityStatus() + object StartingService : ConnectivityStatus() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PokeyReceiver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PokeyReceiver.kt index 688c42515..36b457cd5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PokeyReceiver.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PokeyReceiver.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.service.notifications -import android.app.Application import android.content.BroadcastReceiver import android.content.Context import android.content.Context.RECEIVER_EXPORTED @@ -28,8 +27,12 @@ import android.content.Intent import android.content.IntentFilter import android.os.Build import android.util.Log -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.quartz.nip01Core.core.Event +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.launch class PokeyReceiver : BroadcastReceiver() { @@ -39,7 +42,14 @@ class PokeyReceiver : BroadcastReceiver() { val INTENT = IntentFilter(POKEY_ACTION) } - fun register(app: Application) { + val exceptionHandler = + CoroutineExceptionHandler { _, throwable -> + Log.e("AmethystCoroutine", "Caught exception: ${throwable.message}", throwable) + } + + val scope = CoroutineScope(Dispatchers.Default + SupervisorJob() + exceptionHandler) + + fun register(app: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { app.registerReceiver(this, INTENT, RECEIVER_EXPORTED) } else { @@ -48,8 +58,9 @@ class PokeyReceiver : BroadcastReceiver() { } } - fun unregister(app: Application) { + fun unregister(app: Context) { app.unregisterReceiver(this) + scope.cancel() } override fun onReceive( @@ -62,13 +73,9 @@ class PokeyReceiver : BroadcastReceiver() { if (eventStr == null) return - val app = context.applicationContext as Amethyst - - app.applicationIOScope.launch { + scope.launch { try { - EventNotificationConsumer(app).findAccountAndConsume( - Event.fromJson(eventStr), - ) + EventNotificationConsumer(context.applicationContext).findAccountAndConsume(Event.fromJson(eventStr)) } catch (e: Exception) { Log.e(TAG, "Failed to parse Pokey Event", e) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt index 76c75b9d2..0b9423834 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt @@ -62,7 +62,7 @@ fun VideoViewInner( callbackUri = nostrUriCallback, mimeType = mimeType, aspectRatio = aspectRatio, - proxyPort = accountViewModel.proxyPortForVideo(videoUri), + proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(videoUri), keepPlaying = true, waveformData = waveform, ) { mediaItem -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt index 376183606..56860511c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt @@ -25,7 +25,6 @@ import com.vitorpamplona.amethyst.model.torState.TorRelayEvaluation import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager import com.vitorpamplona.amethyst.service.connectivity.ConnectivityStatus import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager -import com.vitorpamplona.amethyst.service.okhttp.ProxySettingsAnchor import com.vitorpamplona.amethyst.ui.tor.TorManager import com.vitorpamplona.amethyst.ui.tor.TorServiceStatus import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient @@ -45,7 +44,7 @@ import net.freehaven.tor.control.TorControlCommands import okhttp3.OkHttpClient class RelayProxyClientConnector( - val torProxySettingsAnchor: ProxySettingsAnchor, + val torEvaluator: StateFlow, val okHttpClients: DualHttpClientManager, val connManager: ConnectivityManager, val client: INostrClient, @@ -53,7 +52,7 @@ class RelayProxyClientConnector( val scope: CoroutineScope, ) { data class RelayServiceInfra( - val torSettings: StateFlow, + val evaluator: TorRelayEvaluation, val torConnection: OkHttpClient, val clearConnection: OkHttpClient, val connectivity: ConnectivityStatus, @@ -62,7 +61,7 @@ class RelayProxyClientConnector( @OptIn(FlowPreview::class) val relayServices = combine( - torProxySettingsAnchor.flow, + torEvaluator, okHttpClients.defaultHttpClient, okHttpClients.defaultHttpClientWithoutProxy, connManager.status, @@ -70,7 +69,9 @@ class RelayProxyClientConnector( RelayServiceInfra(torSettings, torConnection, clearConnection, connectivity) }.debounce(100) .onEach { - if (it.connectivity is ConnectivityStatus.Off) { + if (it.connectivity is ConnectivityStatus.StartingService) { + // ignore + } else if (it.connectivity is ConnectivityStatus.Off) { Log.d("ManageRelayServices", "Pausing Relay Services ${it.connectivity}") if (client.isActive()) { client.disconnect() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt index 4423bd16c..94d4287aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt @@ -152,7 +152,7 @@ class UploadOrchestrator { alt = alt, sensitiveContent = contentWarningReason, serverBaseUrl = serverBaseUrl, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, onProgress = { percent: Float -> updateState(0.2 + (0.2 * percent), UploadingState.Uploading) }, @@ -165,7 +165,7 @@ class UploadOrchestrator { localContentType = contentType, originalContentType = contentTypeForResult, originalHash = originalHash, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, ) } catch (_: SignerExceptions.ReadOnlyException) { error(R.string.login_with_a_private_key_to_be_able_to_upload) @@ -198,7 +198,7 @@ class UploadOrchestrator { alt = alt, sensitiveContent = contentWarningReason, serverBaseUrl = serverBaseUrl, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, httpAuth = account::createBlossomUploadAuth, context = context, ) @@ -206,7 +206,7 @@ class UploadOrchestrator { verifyHeader( uploadResult = result, localContentType = contentType, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, originalHash = originalHash, originalContentType = contentTypeForResult, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 3b299a21d..2b8a9e1f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -28,6 +28,7 @@ import androidx.activity.enableEdgeToEdge import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.debugState @@ -40,7 +41,6 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.AccountScreen import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel -import com.vitorpamplona.amethyst.ui.screen.prepareSharedViewModel import com.vitorpamplona.amethyst.ui.theme.AmethystTheme import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser @@ -67,16 +67,14 @@ class MainActivity : AppCompatActivity() { setContent { StringResSetup() - - val sharedPreferencesViewModel = prepareSharedViewModel() - AmethystTheme(sharedPreferencesViewModel.sharedPrefs.theme) { + AmethystTheme { val accountStateViewModel: AccountStateViewModel = viewModel() LaunchedEffect(key1 = Unit) { accountStateViewModel.loginWithDefaultAccountIfLoggedOff() } - AccountScreen(accountStateViewModel, sharedPreferencesViewModel) + AccountScreen(accountStateViewModel) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/CrossfadeIfEnabled.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/CrossfadeIfEnabled.kt index ec89215f0..3a316761d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/CrossfadeIfEnabled.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/CrossfadeIfEnabled.kt @@ -37,7 +37,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.util.fastForEach -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable @@ -50,7 +49,7 @@ fun CrossfadeIfEnabled( accountViewModel: AccountViewModel, content: @Composable (T) -> Unit, ) { - if (accountViewModel.settings.featureSet == FeatureSetType.PERFORMANCE) { + if (accountViewModel.settings.isPerformanceMode()) { Box(modifier, contentAlignment) { content(targetState) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt index 5d0bc3222..bb6263bd9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt @@ -193,7 +193,7 @@ class NewUserMetadataViewModel : ViewModel() { alt = null, sensitiveContent = null, serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, onProgress = {}, httpAuth = account::createHTTPAuthorization, context = context, @@ -206,7 +206,7 @@ class NewUserMetadataViewModel : ViewModel() { alt = null, sensitiveContent = null, serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, httpAuth = account::createBlossomUploadAuth, context = context, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/WindowUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/WindowUtils.kt index b9f616dd0..fdff9d9c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/WindowUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/WindowUtils.kt @@ -56,6 +56,6 @@ tailrec fun Context.getActivity(): ComponentActivity = } @Composable -fun getAmethystApp(): Amethyst = LocalContext.current.getAmethystApp() +fun amethystApp(): Amethyst = LocalContext.current.asAmethystApp() -fun Context.getAmethystApp(): Amethyst = this.applicationContext as Amethyst +fun Context.asAmethystApp(): Amethyst = this.applicationContext as Amethyst diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt index bfc5b8a36..4e28c413e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt @@ -311,9 +311,9 @@ private suspend fun saveMediaToGallery( mimeType = content.mimeType, okHttpClient = { if (isImage) { - accountViewModel.okHttpClientForImage(it) + accountViewModel.httpClientBuilder.okHttpClientForImage(it) } else { - accountViewModel.okHttpClientForVideo(it) + accountViewModel.httpClientBuilder.okHttpClientForVideo(it) } }, localContext, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt index 968517715..7b7ea6560 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/DisappearingScaffold.kt @@ -52,7 +52,6 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver -import com.vitorpamplona.amethyst.model.BooleanType import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DividerThickness import kotlin.math.abs @@ -70,7 +69,7 @@ fun DisappearingScaffold( val shouldShow = remember { mutableStateOf(isActive()) } val modifier = - if (accountViewModel.settings.automaticallyHideNavigationBars == BooleanType.ALWAYS) { + if (accountViewModel.settings.isImmersiveScrollingActive()) { val bottomBarHeightPx = with(LocalDensity.current) { 50.dp.roundToPx().toFloat() } val bottomBarOffsetHeightPx = remember { mutableFloatStateOf(0f) } @@ -90,7 +89,7 @@ fun DisappearingScaffold( val newOffset = bottomBarOffsetHeightPx.floatValue + available.y - if (accountViewModel.settings.automaticallyHideNavigationBars == BooleanType.ALWAYS) { + if (accountViewModel.settings.isImmersiveScrollingActive()) { val newBottomBarOffset = if (!isInvertedLayout) { newOffset.coerceIn(-bottomBarHeightPx, 0f) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 7ec9b35e1..385f538a0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -39,6 +39,7 @@ import androidx.core.net.toUri import androidx.core.util.Consumer import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.crashreports.DisplayCrashMessages import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.compose.DisplayNotifyMessages @@ -54,7 +55,6 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.isBaseRoute import com.vitorpamplona.amethyst.ui.navigation.routes.isSameRoute import com.vitorpamplona.amethyst.ui.note.nip22Comments.ReplyCommentPostScreen import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel -import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.BookmarkListScreen @@ -106,7 +106,6 @@ import java.net.URI fun AppNavigation( accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel, - sharedPreferencesViewModel: SharedPreferencesViewModel, ) { val nav = rememberNav() @@ -121,16 +120,16 @@ fun AppNavigation( composable { MessagesScreen(accountViewModel, nav) } composable { VideoScreen(accountViewModel, nav) } composable { DiscoverScreen(accountViewModel, nav) } - composable { NotificationScreen(sharedPreferencesViewModel, accountViewModel, nav) } + composable { NotificationScreen(accountViewModel, nav) } composable { NewUserMetadataScreen(nav, accountViewModel) } composable { SearchScreen(accountViewModel, nav) } composableFromEnd { SecurityFiltersScreen(accountViewModel, nav) } - composableFromEnd { PrivacyOptionsScreen(accountViewModel, nav) } + composableFromEnd { PrivacyOptionsScreen(Amethyst.instance.torPrefs.value, nav) } composableFromEnd { BookmarkListScreen(accountViewModel, nav) } composableFromEnd { DraftListScreen(accountViewModel, nav) } - composableFromEnd { SettingsScreen(sharedPreferencesViewModel, accountViewModel, nav) } + composableFromEnd { SettingsScreen(accountViewModel, nav) } composableFromEnd { UserSettingsScreen(accountViewModel, nav) } composableFromBottomArgs { NIP47SetupScreen(accountViewModel, nav, it.nip47) } composableFromEndArgs { AllRelayListScreen(accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt index ca1f0cb77..4bd13e604 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt @@ -57,7 +57,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo @@ -218,7 +217,7 @@ private fun AccountPicture( contentDescription = stringRes(R.string.profile_image), modifier = AccountPictureModifier, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt index 1d2522ec5..3bed8277a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt @@ -91,7 +91,6 @@ import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollowerCount @@ -247,7 +246,7 @@ fun ProfileContentTemplate( .border(3.dp, MaterialTheme.colorScheme.onBackground, CircleShape) .clickable(onClick = onClick), loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), ) if (bestDisplayName != null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/UserDrawerSearchTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/UserDrawerSearchTopBar.kt index f5bfc6660..e7172f76f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/UserDrawerSearchTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/UserDrawerSearchTopBar.kt @@ -32,7 +32,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.navigation.navs.INav @@ -87,7 +86,7 @@ private fun LoggedInUserPictureDrawer( modifier = HeaderPictureModifier, contentScale = ContentScale.Crop, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 6caa3dff4..28844d0dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -63,7 +63,6 @@ import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.User @@ -605,7 +604,7 @@ fun WatchUserMetadataAndFollowsAndRenderUserProfilePicture( modifier = MaterialTheme.colorScheme.profile35dpModifier, contentScale = ContentScale.Crop, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 54de45d75..b2e1a90c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -54,7 +54,6 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.produceCachedStateAsync import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelPicture @@ -544,7 +543,7 @@ fun InnerNoteWithReactions( baseNote.event !is GenericRepostEvent && !isBoostedNote && !isQuotedNote && - accountViewModel.settings.featureSet == FeatureSetType.COMPLETE + accountViewModel.settings.isCompleteUIMode() NoteBody( baseNote = baseNote, showAuthorPicture = isQuotedNote, @@ -1256,7 +1255,7 @@ fun BadgeBox( accountViewModel: AccountViewModel, nav: INav, ) { - if (accountViewModel.settings.featureSet == FeatureSetType.COMPLETE) { + if (accountViewModel.settings.isCompleteUIMode()) { if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) { baseNote.replyTo?.lastOrNull()?.let { RelayBadges(it, accountViewModel, nav) } } else { @@ -1309,7 +1308,7 @@ private fun ChannelNotePicture( contentDescription = stringRes(R.string.group_picture), modifier = MaterialTheme.colorScheme.channelNotePictureModifier, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 24a9b4f15..20560510c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -96,7 +96,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.ZapPaymentHandler @@ -670,7 +669,7 @@ private fun SlidingAnimationCount( textColor: Color, accountViewModel: AccountViewModel, ) { - if (accountViewModel.settings.featureSet == FeatureSetType.PERFORMANCE) { + if (accountViewModel.settings.isPerformanceMode()) { TextCount(baseCount, textColor) } else { AnimatedContent( @@ -719,7 +718,7 @@ fun SlidingAnimationAmount( textColor: Color, accountViewModel: AccountViewModel, ) { - if (accountViewModel.settings.featureSet == FeatureSetType.PERFORMANCE) { + if (accountViewModel.settings.isPerformanceMode()) { Text( text = amount, fontSize = Font14SP, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt index f67138bb5..d39042564 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt @@ -50,14 +50,13 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.LargeRelayIconModifier import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter @@ -126,7 +125,7 @@ fun RenderRelay( accountViewModel: AccountViewModel, nav: INav, ) { - val relayInfo by loadRelayInfo(relay, accountViewModel) + val relayInfo by loadRelayInfo(relay) val clipboardManager = LocalClipboardManager.current val clickableModifier = @@ -152,7 +151,7 @@ fun RenderRelay( iconUrl = relayInfo.icon, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, pingInMs = 0, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index 3cfbaec4a..0ee7c5ba5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -36,7 +36,6 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.Dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo @@ -94,7 +93,7 @@ fun DisplayBlankAuthor( robot = "authornotfound", contentDescription = stringRes(R.string.unknown_author), modifier = nullModifier, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), ) } @@ -360,7 +359,7 @@ fun InnerUserPicture( modifier = myImageModifier, contentScale = ContentScale.Crop, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 43ce9e53b..4a4772c2b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -638,10 +638,7 @@ open class CommentPostViewModel : viewModelScope.launch(Dispatchers.IO) { iMetaAttachments.downloadAndPrepare(item.link.url) { - Amethyst.Companion.instance.okHttpClients - .getHttpClient( - accountViewModel.account.privacyState.shouldUseTorForImageDownload(item.link.url), - ) + Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt index b70ba751e..afe51ac8d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt @@ -61,7 +61,6 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent @@ -328,7 +327,7 @@ fun ShortCommunityHeader( contentScale = ContentScale.Crop, modifier = HeaderPictureModifier, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), ) } @@ -378,7 +377,7 @@ fun ShortCommunityHeaderNoActions( contentScale = ContentScale.Crop, modifier = HeaderPictureModifier, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt index a63c56721..0a0210fb0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt @@ -121,7 +121,7 @@ fun VoiceHeader( callbackUri = callbackUri, mimeType = null, aspectRatio = null, - proxyPort = accountViewModel.proxyPortForVideo(media), + proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(media), keepPlaying = false, waveformData = waveform, ) { mediaItem -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt index 73041ccc0..d5a053240 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt @@ -34,16 +34,17 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoggedInPage import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginOrSignupScreen import com.vitorpamplona.amethyst.ui.stringRes @Composable -fun AccountScreen( - accountStateViewModel: AccountStateViewModel, - sharedPreferencesViewModel: SharedPreferencesViewModel, -) { +fun AccountScreen(accountStateViewModel: AccountStateViewModel) { + // Pauses relay services when the app pauses + ManageRelayServices() + val accountState by accountStateViewModel.accountContent.collectAsStateWithLifecycle() Log.d("ActivityLifecycle", "AccountScreen $accountState $accountStateViewModel") @@ -55,11 +56,17 @@ fun AccountScreen( when (state) { is AccountState.Loading -> LoadingSetup() is AccountState.LoggedOff -> LoggedOffSetup(accountStateViewModel) - is AccountState.LoggedIn -> LoggedInSetup(state, accountStateViewModel, sharedPreferencesViewModel) + is AccountState.LoggedIn -> LoggedInSetup(state, accountStateViewModel) } } } +@Composable +fun ManageRelayServices() { + val relayServices by Amethyst.instance.relayProxyClientConnector.relayServices + .collectAsStateWithLifecycle() +} + @Composable fun LoadingSetup() { // A surface container using the 'background' color from the theme @@ -91,14 +98,12 @@ fun LoggedOffSetup(accountStateViewModel: AccountStateViewModel) { fun LoggedInSetup( state: AccountState.LoggedIn, accountStateViewModel: AccountStateViewModel, - sharedPreferencesViewModel: SharedPreferencesViewModel, ) { SetAccountCentricViewModelStore(state) { LoggedInPage( - state.account, - state.route, - accountStateViewModel, - sharedPreferencesViewModel, + account = state.account, + route = state.route, + accountStateViewModel = accountStateViewModel, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 9378e07a6..573ec8c30 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -35,8 +35,6 @@ import com.vitorpamplona.amethyst.model.DefaultNIP65RelaySet import com.vitorpamplona.amethyst.model.DefaultSearchRelayList import com.vitorpamplona.amethyst.service.Nip05NostrAddressVerifier import com.vitorpamplona.amethyst.ui.navigation.routes.Route -import com.vitorpamplona.amethyst.ui.tor.TorSettings -import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair @@ -108,7 +106,6 @@ class AccountStateViewModel : ViewModel() { suspend fun loginAndStartUI( key: String, - torSettings: TorSettings, transientAccount: Boolean, loginWithExternalSigner: Boolean = false, packageName: String = "", @@ -142,31 +139,26 @@ class AccountStateViewModel : ViewModel() { keyPair = KeyPair(pubKey = pubKeyParsed), transientAccount = transientAccount, externalSignerPackageName = packageName.ifBlank { "com.greenart7c3.nostrsigner" }, - torSettings = TorSettingsFlow.build(torSettings), ) } else if (key.startsWith("nsec")) { AccountSettings( keyPair = KeyPair(privKey = key.bechToBytes()), transientAccount = transientAccount, - torSettings = TorSettingsFlow.build(torSettings), ) } else if (key.contains(" ") && Nip06().isValidMnemonic(key)) { AccountSettings( keyPair = KeyPair(privKey = Nip06().privateKeyFromMnemonic(key)), transientAccount = transientAccount, - torSettings = TorSettingsFlow.build(torSettings), ) } else if (pubKeyParsed != null) { AccountSettings( keyPair = KeyPair(pubKey = pubKeyParsed), transientAccount = transientAccount, - torSettings = TorSettingsFlow.build(torSettings), ) } else { AccountSettings( keyPair = KeyPair(Hex.decode(key)), transientAccount = transientAccount, - torSettings = TorSettingsFlow.build(torSettings), ) } @@ -197,7 +189,6 @@ class AccountStateViewModel : ViewModel() { fun login( key: String, password: String, - torSettings: TorSettings, transientAccount: Boolean, loginWithExternalSigner: Boolean = false, packageName: String = "", @@ -222,41 +213,39 @@ class AccountStateViewModel : ViewModel() { onError("Could not decrypt key with provided password") Log.e("Login", "Could not decrypt ncryptsec") } else { - loginSync(newKey, torSettings, transientAccount, loginWithExternalSigner, packageName, onError) + loginSync(newKey, transientAccount, loginWithExternalSigner, packageName, onError) } } else if (EMAIL_PATTERN.matcher(key).matches()) { Nip05NostrAddressVerifier().verifyNip05( key, okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(false) }, onSuccess = { publicKey -> - loginSync(Hex.decode(publicKey).toNpub(), torSettings, transientAccount, loginWithExternalSigner, packageName, onError) + loginSync(Hex.decode(publicKey).toNpub(), transientAccount, loginWithExternalSigner, packageName, onError) }, onError = { onError(it) }, ) } else { - loginSync(key, torSettings, transientAccount, loginWithExternalSigner, packageName, onError) + loginSync(key, transientAccount, loginWithExternalSigner, packageName, onError) } } } fun login( key: String, - torSettings: TorSettings, transientAccount: Boolean, loginWithExternalSigner: Boolean = false, packageName: String = "", onError: (String) -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { - loginSync(key, torSettings, transientAccount, loginWithExternalSigner, packageName, onError) + loginSync(key, transientAccount, loginWithExternalSigner, packageName, onError) } } suspend fun loginSync( key: String, - torSettings: TorSettings, transientAccount: Boolean, loginWithExternalSigner: Boolean = false, packageName: String = "", @@ -266,7 +255,7 @@ class AccountStateViewModel : ViewModel() { if (_accountContent.value is AccountState.LoggedIn) { prepareLogoutOrSwitch() } - loginAndStartUI(key, torSettings, transientAccount, loginWithExternalSigner, packageName) + loginAndStartUI(key, transientAccount, loginWithExternalSigner, packageName) } catch (e: Exception) { if (e is CancellationException) throw e Log.e("Login", "Could not sign in", e) @@ -274,10 +263,7 @@ class AccountStateViewModel : ViewModel() { } } - fun newKey( - torSettings: TorSettings, - name: String? = null, - ) { + fun newKey(name: String? = null) { viewModelScope.launch(Dispatchers.IO) { if (_accountContent.value is AccountState.LoggedIn) { prepareLogoutOrSwitch() @@ -285,7 +271,7 @@ class AccountStateViewModel : ViewModel() { _accountContent.update { AccountState.Loading } - val accountSettings = createNewAccount(torSettings, name) + val accountSettings = createNewAccount(name) LocalPreferences.setDefaultAccount(accountSettings) @@ -306,10 +292,7 @@ class AccountStateViewModel : ViewModel() { } } - fun createNewAccount( - torSettings: TorSettings, - name: String? = null, - ): AccountSettings { + fun createNewAccount(name: String? = null): AccountSettings { val keyPair = KeyPair() val tempSigner = NostrSignerSync(keyPair) @@ -327,7 +310,6 @@ class AccountStateViewModel : ViewModel() { backupDMRelayList = ChatMessageRelayListEvent.create(DefaultDMRelayList, tempSigner), backupSearchRelayList = SearchRelayListEvent.create(DefaultSearchRelayList.toList(), tempSigner), backupChannelList = ChannelListEvent.create(emptyList(), DefaultChannels, tempSigner), - torSettings = TorSettingsFlow.build(torSettings), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesComposables.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesComposables.kt deleted file mode 100644 index 4fc932da1..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesComposables.kt +++ /dev/null @@ -1,72 +0,0 @@ -/** - * 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 - -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.platform.LocalContext -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel -import com.google.accompanist.adaptive.calculateDisplayFeatures -import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.service.connectivity.ConnectivityStatus -import com.vitorpamplona.amethyst.ui.components.getActivity - -@Composable -fun prepareSharedViewModel(): SharedPreferencesViewModel { - val sharedPreferencesViewModel: SharedPreferencesViewModel = viewModel() - - LaunchedEffect(key1 = sharedPreferencesViewModel) { - sharedPreferencesViewModel.init() - } - - MonitorDisplaySize(sharedPreferencesViewModel) - ManageConnectivity(sharedPreferencesViewModel) - - return sharedPreferencesViewModel -} - -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) -@Composable -fun MonitorDisplaySize(sharedPreferencesViewModel: SharedPreferencesViewModel) { - val act = LocalContext.current.getActivity() - - val displayFeatures = calculateDisplayFeatures(act) - val windowSizeClass = calculateWindowSizeClass(act) - - LaunchedEffect(sharedPreferencesViewModel, displayFeatures, windowSizeClass) { - sharedPreferencesViewModel.updateDisplaySettings(windowSizeClass, displayFeatures) - } -} - -@Composable -fun ManageConnectivity(sharedPreferencesViewModel: SharedPreferencesViewModel) { - val status = - Amethyst.instance.connManager.status - .collectAsStateWithLifecycle() - - (status.value as? ConnectivityStatus.Active)?.let { - sharedPreferencesViewModel.updateNetworkState(it.networkId) - sharedPreferencesViewModel.updateConnectivityStatusState(it.isMobile) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesViewModel.kt deleted file mode 100644 index dffe10581..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesViewModel.kt +++ /dev/null @@ -1,211 +0,0 @@ -/** - * 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 - -import android.util.Log -import androidx.appcompat.app.AppCompatDelegate -import androidx.compose.material3.windowsizeclass.WindowSizeClass -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.core.os.LocaleListCompat -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.window.layout.DisplayFeature -import com.vitorpamplona.amethyst.LocalPreferences -import com.vitorpamplona.amethyst.model.BooleanType -import com.vitorpamplona.amethyst.model.ConnectivityType -import com.vitorpamplona.amethyst.model.FeatureSetType -import com.vitorpamplona.amethyst.model.ProfileGalleryType -import com.vitorpamplona.amethyst.model.Settings -import com.vitorpamplona.amethyst.model.ThemeType -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -@Stable -class SharedPreferencesViewModel : ViewModel() { - val sharedPrefs: SharedSettingsState = SharedSettingsState() - - fun init() { - viewModelScope.launch(Dispatchers.IO) { - Log.d("SharedPreferencesViewModel", "init") - val savedSettings = - LocalPreferences.loadSharedSettings() ?: Settings() - - sharedPrefs.theme = savedSettings.theme - sharedPrefs.language = savedSettings.preferredLanguage - sharedPrefs.automaticallyShowImages = savedSettings.automaticallyShowImages - sharedPrefs.automaticallyStartPlayback = savedSettings.automaticallyStartPlayback - sharedPrefs.automaticallyShowUrlPreview = savedSettings.automaticallyShowUrlPreview - sharedPrefs.automaticallyHideNavigationBars = savedSettings.automaticallyHideNavigationBars - sharedPrefs.automaticallyShowProfilePictures = savedSettings.automaticallyShowProfilePictures - sharedPrefs.dontShowPushNotificationSelector = savedSettings.dontShowPushNotificationSelector - sharedPrefs.dontAskForNotificationPermissions = savedSettings.dontAskForNotificationPermissions - sharedPrefs.gallerySet = savedSettings.gallerySet - sharedPrefs.featureSet = savedSettings.featureSet - - updateLanguageInTheUI() - } - } - - fun updateTheme(newTheme: ThemeType) { - if (sharedPrefs.theme != newTheme) { - sharedPrefs.theme = newTheme - - saveSharedSettings() - } - } - - fun updateLanguage(newLanguage: String?) { - if (sharedPrefs.language != newLanguage) { - sharedPrefs.language = newLanguage - updateLanguageInTheUI() - saveSharedSettings() - } - } - - fun updateLanguageInTheUI() { - if (sharedPrefs.language != null) { - viewModelScope.launch(Dispatchers.Main) { - AppCompatDelegate.setApplicationLocales( - LocaleListCompat.forLanguageTags(sharedPrefs.language), - ) - } - } - } - - fun updateAutomaticallyStartPlayback(newAutomaticallyStartPlayback: ConnectivityType) { - if (sharedPrefs.automaticallyStartPlayback != newAutomaticallyStartPlayback) { - sharedPrefs.automaticallyStartPlayback = newAutomaticallyStartPlayback - saveSharedSettings() - } - } - - fun updateAutomaticallyShowUrlPreview(newAutomaticallyShowUrlPreview: ConnectivityType) { - if (sharedPrefs.automaticallyShowUrlPreview != newAutomaticallyShowUrlPreview) { - sharedPrefs.automaticallyShowUrlPreview = newAutomaticallyShowUrlPreview - saveSharedSettings() - } - } - - fun updateAutomaticallyShowProfilePicture(newAutomaticallyShowProfilePictures: ConnectivityType) { - if (sharedPrefs.automaticallyShowProfilePictures != newAutomaticallyShowProfilePictures) { - sharedPrefs.automaticallyShowProfilePictures = newAutomaticallyShowProfilePictures - saveSharedSettings() - } - } - - fun updateAutomaticallyHideNavBars(newAutomaticallyHideHavBars: BooleanType) { - if (sharedPrefs.automaticallyHideNavigationBars != newAutomaticallyHideHavBars) { - sharedPrefs.automaticallyHideNavigationBars = newAutomaticallyHideHavBars - saveSharedSettings() - } - } - - fun updateAutomaticallyShowImages(newAutomaticallyShowImages: ConnectivityType) { - if (sharedPrefs.automaticallyShowImages != newAutomaticallyShowImages) { - sharedPrefs.automaticallyShowImages = newAutomaticallyShowImages - saveSharedSettings() - } - } - - fun updateFeatureSetType(newFeatureSetType: FeatureSetType) { - if (sharedPrefs.featureSet != newFeatureSetType) { - sharedPrefs.featureSet = newFeatureSetType - saveSharedSettings() - } - } - - fun updateGallerySetType(newgalleryType: ProfileGalleryType) { - if (sharedPrefs.gallerySet != newgalleryType) { - sharedPrefs.gallerySet = newgalleryType - saveSharedSettings() - } - } - - fun dontShowPushNotificationSelector() { - if (sharedPrefs.dontShowPushNotificationSelector == false) { - sharedPrefs.dontShowPushNotificationSelector = true - saveSharedSettings() - } - } - - fun dontAskForNotificationPermissions() { - if (sharedPrefs.dontAskForNotificationPermissions == false) { - sharedPrefs.dontAskForNotificationPermissions = true - saveSharedSettings() - } - } - - fun updateConnectivityStatusState(isOnMobileDataState: Boolean) { - if (sharedPrefs.isOnMobileOrMeteredConnection != isOnMobileDataState) { - Log.d("Connectivity", "updateConnectivityStatusState ${sharedPrefs.currentNetworkId}: ${sharedPrefs.isOnMobileOrMeteredConnection} -> $isOnMobileDataState") - sharedPrefs.isOnMobileOrMeteredConnection = isOnMobileDataState - } - } - - fun updateNetworkState(networkId: Long) { - if (sharedPrefs.currentNetworkId != networkId) { - Log.d("Connectivity", "updateNetworkState ${sharedPrefs.currentNetworkId} -> $networkId") - sharedPrefs.currentNetworkId = networkId - } - } - - fun updateDisplaySettings( - windowSizeClass: WindowSizeClass, - displayFeatures: List, - ) { - if (sharedPrefs.windowSizeClass.value != windowSizeClass) { - sharedPrefs.windowSizeClass.value = windowSizeClass - } - if (sharedPrefs.displayFeatures.value != displayFeatures) { - sharedPrefs.displayFeatures.value = displayFeatures - } - } - - fun saveSharedSettings() { - viewModelScope.launch(Dispatchers.IO) { - Log.d("SharedPreferencesViewModel", "Saving Shared Settings") - LocalPreferences.saveSharedSettings( - Settings( - sharedPrefs.theme, - sharedPrefs.language, - sharedPrefs.automaticallyShowImages, - sharedPrefs.automaticallyStartPlayback, - sharedPrefs.automaticallyShowUrlPreview, - sharedPrefs.automaticallyHideNavigationBars, - sharedPrefs.automaticallyShowProfilePictures, - sharedPrefs.dontShowPushNotificationSelector, - sharedPrefs.dontAskForNotificationPermissions, - sharedPrefs.featureSet, - sharedPrefs.gallerySet, - ), - ) - } - } -} - -@Composable -fun mockSharedPreferencesViewModel(): SharedPreferencesViewModel { - val sharedPreferencesViewModel: SharedPreferencesViewModel = viewModel() - sharedPreferencesViewModel.init() - return sharedPreferencesViewModel -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedSettingsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedSettingsState.kt deleted file mode 100644 index cbdd75954..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedSettingsState.kt +++ /dev/null @@ -1,101 +0,0 @@ -/** - * 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 - -import androidx.compose.material3.windowsizeclass.WindowSizeClass -import androidx.compose.runtime.Stable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableLongStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.window.layout.DisplayFeature -import com.vitorpamplona.amethyst.model.BooleanType -import com.vitorpamplona.amethyst.model.ConnectivityType -import com.vitorpamplona.amethyst.model.FeatureSetType -import com.vitorpamplona.amethyst.model.ProfileGalleryType -import com.vitorpamplona.amethyst.model.ThemeType - -@Stable -class SharedSettingsState { - var theme by mutableStateOf(ThemeType.SYSTEM) - var language by mutableStateOf(null) - - var automaticallyShowImages by mutableStateOf(ConnectivityType.ALWAYS) - var automaticallyStartPlayback by mutableStateOf(ConnectivityType.ALWAYS) - var automaticallyShowUrlPreview by mutableStateOf(ConnectivityType.ALWAYS) - var automaticallyHideNavigationBars by mutableStateOf(BooleanType.ALWAYS) - var automaticallyShowProfilePictures by mutableStateOf(ConnectivityType.ALWAYS) - var dontShowPushNotificationSelector by mutableStateOf(false) - var dontAskForNotificationPermissions by mutableStateOf(false) - var featureSet by mutableStateOf(FeatureSetType.SIMPLIFIED) - var gallerySet by mutableStateOf(ProfileGalleryType.CLASSIC) - - var isOnMobileOrMeteredConnection by mutableStateOf(false) - var currentNetworkId by mutableLongStateOf(0L) - - var windowSizeClass = mutableStateOf(null) - var displayFeatures = mutableStateOf>(emptyList()) - - val showProfilePictures = - derivedStateOf { - when (automaticallyShowProfilePictures) { - ConnectivityType.WIFI_ONLY -> !isOnMobileOrMeteredConnection - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } - } - - val modernGalleryStyle = - derivedStateOf { - when (gallerySet) { - ProfileGalleryType.CLASSIC -> false - ProfileGalleryType.MODERN -> true - } - } - - val showUrlPreview = - derivedStateOf { - when (automaticallyShowUrlPreview) { - ConnectivityType.WIFI_ONLY -> !isOnMobileOrMeteredConnection - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } - } - - val startVideoPlayback = - derivedStateOf { - when (automaticallyStartPlayback) { - ConnectivityType.WIFI_ONLY -> !isOnMobileOrMeteredConnection - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } - } - - val showImages = - derivedStateOf { - when (automaticallyShowImages) { - ConnectivityType.WIFI_ONLY -> !isOnMobileOrMeteredConnection - ConnectivityType.NEVER -> false - ConnectivityType.ALWAYS -> true - } - } -} 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 new file mode 100644 index 000000000..2e9fa918c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UiSettingsState.kt @@ -0,0 +1,134 @@ +/** + * 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 + +import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.model.BooleanType +import com.vitorpamplona.amethyst.model.ConnectivityType +import com.vitorpamplona.amethyst.model.FeatureSetType +import com.vitorpamplona.amethyst.model.ProfileGalleryType +import com.vitorpamplona.amethyst.model.UiSettingsFlow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn + +@Stable +class UiSettingsState( + val uiSettingsFlow: UiSettingsFlow, + val isMobileOrMeteredConnection: StateFlow, + val scope: CoroutineScope, +) { + val showProfilePictures = + combine( + uiSettingsFlow.automaticallyShowProfilePictures, + isMobileOrMeteredConnection, + ) { showProfilePictures, isOnMobileOrMeteredConnection -> + when (showProfilePictures) { + ConnectivityType.WIFI_ONLY -> !isOnMobileOrMeteredConnection + ConnectivityType.NEVER -> false + ConnectivityType.ALWAYS -> true + } + }.stateIn( + scope, + SharingStarted.Eagerly, + when (uiSettingsFlow.automaticallyShowProfilePictures.value) { + ConnectivityType.WIFI_ONLY -> !isMobileOrMeteredConnection.value + ConnectivityType.NEVER -> false + ConnectivityType.ALWAYS -> true + }, + ) + + val showUrlPreview = + combine( + uiSettingsFlow.automaticallyShowUrlPreview, + isMobileOrMeteredConnection, + ) { automaticallyShowUrlPreview, isOnMobileOrMeteredConnection -> + when (automaticallyShowUrlPreview) { + ConnectivityType.WIFI_ONLY -> !isOnMobileOrMeteredConnection + ConnectivityType.NEVER -> false + ConnectivityType.ALWAYS -> true + } + }.stateIn( + scope, + SharingStarted.Eagerly, + when (uiSettingsFlow.automaticallyShowUrlPreview.value) { + ConnectivityType.WIFI_ONLY -> !isMobileOrMeteredConnection.value + ConnectivityType.NEVER -> false + ConnectivityType.ALWAYS -> true + }, + ) + + val startVideoPlayback = + combine( + uiSettingsFlow.automaticallyStartPlayback, + isMobileOrMeteredConnection, + ) { automaticallyStartPlayback, isOnMobileOrMeteredConnection -> + when (automaticallyStartPlayback) { + ConnectivityType.WIFI_ONLY -> !isOnMobileOrMeteredConnection + ConnectivityType.NEVER -> false + ConnectivityType.ALWAYS -> true + } + }.stateIn( + scope, + SharingStarted.Eagerly, + when (uiSettingsFlow.automaticallyStartPlayback.value) { + ConnectivityType.WIFI_ONLY -> !isMobileOrMeteredConnection.value + ConnectivityType.NEVER -> false + ConnectivityType.ALWAYS -> true + }, + ) + + val showImages = + combine( + uiSettingsFlow.automaticallyShowImages, + isMobileOrMeteredConnection, + ) { automaticallyShowImages, isOnMobileOrMeteredConnection -> + when (automaticallyShowImages) { + ConnectivityType.WIFI_ONLY -> !isOnMobileOrMeteredConnection + ConnectivityType.NEVER -> false + ConnectivityType.ALWAYS -> true + } + }.stateIn( + scope, + SharingStarted.Eagerly, + when (uiSettingsFlow.automaticallyShowImages.value) { + ConnectivityType.WIFI_ONLY -> !isMobileOrMeteredConnection.value + ConnectivityType.NEVER -> false + ConnectivityType.ALWAYS -> true + }, + ) + + fun modernGalleryStyle() = + when (uiSettingsFlow.gallerySet.value) { + ProfileGalleryType.CLASSIC -> false + ProfileGalleryType.MODERN -> true + } + + fun isPerformanceMode() = uiSettingsFlow.featureSet.value == FeatureSetType.PERFORMANCE + + fun isNotPerformanceMode() = uiSettingsFlow.featureSet.value != FeatureSetType.PERFORMANCE + + fun isCompleteUIMode() = uiSettingsFlow.featureSet.value == FeatureSetType.COMPLETE + + fun isImmersiveScrollingActive() = uiSettingsFlow.automaticallyHideNavigationBars.value == BooleanType.ALWAYS +} 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 1596e1031..896f98841 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 @@ -33,7 +33,6 @@ import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.compose.viewModel import coil3.asDrawable import coil3.imageLoader import coil3.request.ImageRequest @@ -49,6 +48,7 @@ import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel @@ -56,9 +56,10 @@ import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.observables.CreatedAtComparator +import com.vitorpamplona.amethyst.model.privacyOptions.EmptyRoleBasedHttpClientBuilder +import com.vitorpamplona.amethyst.model.privacyOptions.IRoleBasedHttpClientBuilder +import com.vitorpamplona.amethyst.model.privacyOptions.RoleBasedHttpClientBuilder import com.vitorpamplona.amethyst.service.Nip05NostrAddressVerifier -import com.vitorpamplona.amethyst.service.Nip11CachedRetriever -import com.vitorpamplona.amethyst.service.Nip11Retriever import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.cashu.CashuToken @@ -66,8 +67,6 @@ import com.vitorpamplona.amethyst.service.cashu.melt.MeltProcessor import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.service.location.LocationState -import com.vitorpamplona.amethyst.service.okhttp.EmptyHttpClientManager -import com.vitorpamplona.amethyst.service.okhttp.IHttpClientManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler import com.vitorpamplona.amethyst.service.uploads.CompressorQuality @@ -84,12 +83,12 @@ import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.note.showAmountInteger -import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel -import com.vitorpamplona.amethyst.ui.screen.SharedSettingsState +import com.vitorpamplona.amethyst.ui.screen.UiSettingsState import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedState import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CombinedZap import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.tor.TorSettings +import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow +import com.vitorpamplona.amethyst.ui.tor.TorType import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent @@ -106,8 +105,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser -import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache -import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation +import com.vitorpamplona.quartz.nip03Timestamp.DefaultOtsResolverBuilder import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent @@ -160,15 +158,15 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient import java.util.Locale @Stable class AccountViewModel( val account: Account, - val settings: SharedSettingsState, + val settings: UiSettingsState, + val torSettings: TorSettingsFlow, val dataSources: RelaySubscriptionsCoordinator, - val okHttpClient: IHttpClientManager, + val httpClientBuilder: IRoleBasedHttpClientBuilder, ) : ViewModel(), Dao { var firstRoute: Route? = null @@ -650,7 +648,7 @@ class AccountViewModel( message = message, context = context, showErrorIfNoLnAddress = showErrorIfNoLnAddress, - okHttpClient = ::okHttpClientForMoney, + okHttpClient = httpClientBuilder::okHttpClientForMoney, onError = onError, onProgress = onProgress, onPayViaIntent = onPayViaIntent, @@ -886,7 +884,7 @@ class AccountViewModel( onResult: suspend (UrlPreviewState) -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { - UrlCachedPreviewer.previewInfo(url, ::okHttpClientForPreview, onResult) + UrlCachedPreviewer.previewInfo(url, httpClientBuilder::okHttpClientForPreview, onResult) } } @@ -907,9 +905,7 @@ class AccountViewModel( Nip05NostrAddressVerifier() .verifyNip05( nip05, - okHttpClient = { - okHttpClient.getHttpClient(account.privacyState.shouldUseTorForNIP05(it)) - }, + okHttpClient = httpClientBuilder::okHttpClientForNip05, onSuccess = { // Marks user as verified if (it == pubkeyHex) { @@ -936,21 +932,6 @@ class AccountViewModel( } } - fun retrieveRelayDocument( - relay: NormalizedRelayUrl, - onInfo: (Nip11RelayInformation) -> Unit, - onError: (NormalizedRelayUrl, Nip11Retriever.ErrorCode, String?) -> Unit, - ) { - viewModelScope.launch(Dispatchers.IO) { - Nip11CachedRetriever.loadRelayInfo( - relay, - okHttpClient = { okHttpClientForClean(relay) }, - onInfo, - onError, - ) - } - } - fun runOnIO(runOnIO: () -> Unit) { viewModelScope.launch(Dispatchers.IO) { runOnIO() } } @@ -1073,7 +1054,7 @@ class AccountViewModel( suspend fun checkVideoIsOnline(videoUrl: String): Boolean = withContext(Dispatchers.IO) { - OnlineChecker.isOnline(videoUrl, ::okHttpClientForVideo) + OnlineChecker.isOnline(videoUrl, httpClientBuilder::okHttpClientForVideo) } fun loadAndMarkAsRead( @@ -1108,19 +1089,19 @@ class AccountViewModel( } } - fun setTorSettings(newTorSettings: TorSettings) = runIOCatching { account.settings.setTorSettings(newTorSettings) } - class Factory( val account: Account, - val settings: SharedSettingsState, + val settings: UiSettingsState, + val torSettings: TorSettingsFlow, val dataSources: RelaySubscriptionsCoordinator, - val okHttpClient: IHttpClientManager, + val okHttpClient: RoleBasedHttpClientBuilder, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T = AccountViewModel( account, settings, + torSettings, dataSources, okHttpClient, ) as T @@ -1278,7 +1259,7 @@ class AccountViewModel( if (lud16 != null) { viewModelScope.launch(Dispatchers.IO) { try { - val meltResult = MeltProcessor().melt(token, lud16, ::okHttpClientForMoney, context) + val meltResult = MeltProcessor().melt(token, lud16, httpClientBuilder::okHttpClientForMoney, context) onDone( stringRes(context, R.string.cashu_successful_redemption), stringRes( @@ -1395,22 +1376,6 @@ class AccountViewModel( } } - fun proxyPortForVideo(url: String): Int? = okHttpClient.getCurrentProxyPort(account.privacyState.shouldUseTorForVideoDownload(url)) - - fun okHttpClientForNip96(url: String): OkHttpClient = okHttpClient.getHttpClient(account.privacyState.shouldUseTorForUploads(url)) - - fun okHttpClientForImage(url: String): OkHttpClient = okHttpClient.getHttpClient(account.privacyState.shouldUseTorForImageDownload(url)) - - fun okHttpClientForVideo(url: String): OkHttpClient = okHttpClient.getHttpClient(account.privacyState.shouldUseTorForVideoDownload(url)) - - fun okHttpClientForMoney(url: String): OkHttpClient = okHttpClient.getHttpClient(account.privacyState.shouldUseTorForMoneyOperations(url)) - - fun okHttpClientForPreview(url: String): OkHttpClient = okHttpClient.getHttpClient(account.privacyState.shouldUseTorForPreviewUrl(url)) - - fun okHttpClientForClean(url: NormalizedRelayUrl): OkHttpClient = okHttpClient.getHttpClient(account.torRelayState.shouldUseTorForClean(url)) - - fun okHttpClientForPushRegistration(url: String): OkHttpClient = okHttpClient.getHttpClient(account.privacyState.shouldUseTorForTrustedRelays()) - fun dataSources() = dataSources suspend fun createTempDraftNote(noteEvent: DraftWrapEvent): Note? = draftNoteCache.update(noteEvent) @@ -1526,7 +1491,7 @@ class AccountViewModel( milliSats = milliSats, message = message, nostrRequest = zapRequest, - okHttpClient = ::okHttpClientForMoney, + okHttpClient = httpClientBuilder::okHttpClientForMoney, onProgress = onProgress, context = context, ) @@ -1549,7 +1514,7 @@ class AccountViewModel( viewModelScope.launch { MediaSaverToDisk.saveDownloadingIfNeeded( videoUri = videoUri, - okHttpClient = ::okHttpClientForVideo, + okHttpClient = httpClientBuilder::okHttpClientForVideo, mimeType = mimeType, localContext = localContext, onSuccess = { @@ -1668,8 +1633,17 @@ var mockedCache: AccountViewModel? = null fun mockAccountViewModel(): AccountViewModel { mockedCache?.let { return it } - val sharedPreferencesViewModel: SharedPreferencesViewModel = viewModel() - sharedPreferencesViewModel.init() + val otsResolver = DefaultOtsResolverBuilder() + + val scope = rememberCoroutineScope() + + val uiState = + UiSettingsState( + uiSettingsFlow = UiSettingsFlow(), + isMobileOrMeteredConnection = MutableStateFlow(false), + scope = scope, + ) + val keyPair = KeyPair( privKey = Hex.decode("0f761f8a5a481e26f06605a1d9b3e9eba7a107d351f43c43a57469b788274499"), @@ -1677,8 +1651,6 @@ fun mockAccountViewModel(): AccountViewModel { forceReplacePubkey = false, ) - val scope = rememberCoroutineScope() - val client = EmptyNostrClient val nwcFilters = NWCPaymentFilterAssembler(client) @@ -1689,7 +1661,7 @@ fun mockAccountViewModel(): AccountViewModel { signer = NostrSignerInternal(keyPair), geolocationFlow = MutableStateFlow(LocationState.LocationResult.Loading), nwcFilterAssembler = nwcFilters, - otsVerifCache = VerificationStateCache(), + otsResolverBuilder = otsResolver, cache = LocalCache, client = client, scope = scope, @@ -1697,8 +1669,9 @@ fun mockAccountViewModel(): AccountViewModel { return AccountViewModel( account = account, - settings = sharedPreferencesViewModel.sharedPrefs, - okHttpClient = EmptyHttpClientManager, + settings = uiState, + torSettings = TorSettingsFlow(torType = MutableStateFlow(TorType.OFF)), + httpClientBuilder = EmptyRoleBasedHttpClientBuilder(), dataSources = RelaySubscriptionsCoordinator(LocalCache, client, scope), ).also { mockedCache = it @@ -1712,8 +1685,17 @@ var vitorCache: AccountViewModel? = null fun mockVitorAccountViewModel(): AccountViewModel { mockedCache?.let { return it } - val sharedPreferencesViewModel: SharedPreferencesViewModel = viewModel() - sharedPreferencesViewModel.init() + val scope = rememberCoroutineScope() + + val otsResolver = DefaultOtsResolverBuilder() + + val uiState = + UiSettingsState( + uiSettingsFlow = UiSettingsFlow(), + isMobileOrMeteredConnection = MutableStateFlow(false), + scope = scope, + ) + val keyPair = KeyPair( pubKey = Hex.decode("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), @@ -1723,15 +1705,13 @@ fun mockVitorAccountViewModel(): AccountViewModel { val nwcFilters = NWCPaymentFilterAssembler(client) - val scope = rememberCoroutineScope() - val account = Account( settings = AccountSettings(keyPair), signer = NostrSignerInternal(keyPair), geolocationFlow = MutableStateFlow(LocationState.LocationResult.Loading), nwcFilterAssembler = nwcFilters, - otsVerifCache = VerificationStateCache(), + otsResolverBuilder = otsResolver, cache = LocalCache, client = EmptyNostrClient, scope = scope, @@ -1739,8 +1719,9 @@ fun mockVitorAccountViewModel(): AccountViewModel { return AccountViewModel( account = account, - settings = sharedPreferencesViewModel.sharedPrefs, - okHttpClient = EmptyHttpClientManager, + settings = uiState, + torSettings = TorSettingsFlow(torType = MutableStateFlow(TorType.OFF)), + httpClientBuilder = EmptyRoleBasedHttpClientBuilder(), dataSources = RelaySubscriptionsCoordinator(LocalCache, client, scope), ).also { vitorCache = it diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt index 299f4df11..17cf60ed1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt @@ -30,7 +30,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.lifecycle.compose.LifecycleResumeEffect @@ -49,7 +48,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.Account import com.vitorpamplona.amethyst.ui.navigation.AppNavigation import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel -import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssemblerSubscription @@ -62,7 +60,6 @@ fun LoggedInPage( account: Account, route: Route?, accountStateViewModel: AccountStateViewModel, - sharedPreferencesViewModel: SharedPreferencesViewModel, ) { val accountViewModel: AccountViewModel = viewModel( @@ -70,9 +67,10 @@ fun LoggedInPage( factory = AccountViewModel.Factory( account = account, - settings = sharedPreferencesViewModel.sharedPrefs, + settings = Amethyst.instance.uiState, + torSettings = Amethyst.instance.torPrefs.value, dataSources = Amethyst.instance.sources, - okHttpClient = Amethyst.instance.okHttpClients, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder, ), ) @@ -81,12 +79,6 @@ fun LoggedInPage( // Adds this account to the authentication procedures for relays. RelayAuthSubscription(accountViewModel) - // Sets up Coil's Image Loader - ObserveImageLoadingTor(accountViewModel) - - // Sets up the use of Proxy based on this Account's settings - SetProxyDeterminator(accountViewModel) - // Loads account information + DMs and Notifications from Relays. AccountFilterAssemblerSubscription(accountViewModel) @@ -99,9 +91,6 @@ fun LoggedInPage( // Updates local cache of the anti-spam filter choice of this user. ObserveAntiSpamFilterSettings(accountViewModel) - // Pauses relay services when the app pauses - ManageRelayServices(accountViewModel) - // Listens to Amber ListenToExternalSignerIfNeeded(accountViewModel) @@ -111,7 +100,6 @@ fun LoggedInPage( AppNavigation( accountViewModel = accountViewModel, accountStateViewModel = accountStateViewModel, - sharedPreferencesViewModel = sharedPreferencesViewModel, ) } @@ -123,27 +111,6 @@ fun ObserveAntiSpamFilterSettings(accountViewModel: AccountViewModel) { Amethyst.instance.cache.antiSpam.active = isSpamActive } -@Composable -fun SetProxyDeterminator(accountViewModel: AccountViewModel) { - LaunchedEffect(accountViewModel) { - Amethyst.instance.torProxySettingsAnchor.flow - .tryEmit(accountViewModel.account.torRelayState.flow) - } -} - -@Composable -fun ObserveImageLoadingTor(accountViewModel: AccountViewModel) { - LaunchedEffect(accountViewModel) { - Amethyst.instance.setImageLoader(accountViewModel.account.privacyState::shouldUseTorForImageDownload) - } -} - -@Composable -fun ManageRelayServices(accountViewModel: AccountViewModel) { - val relayServices by Amethyst.instance.relayProxyClientConnector.relayServices - .collectAsStateWithLifecycle() -} - @OptIn(ExperimentalPermissionsApi::class) @Composable fun NotificationRegistration(accountViewModel: AccountViewModel) { @@ -158,7 +125,7 @@ fun NotificationRegistration(accountViewModel: AccountViewModel) { scope.launch { PushNotificationUtils.checkAndInit( LocalPreferences.allSavedAccounts(), - accountViewModel::okHttpClientForPushRegistration, + accountViewModel.httpClientBuilder::okHttpClientForPushRegistration, ) } @@ -173,7 +140,7 @@ fun NotificationRegistration(accountViewModel: AccountViewModel) { scope.launch { PushNotificationUtils.checkAndInit( LocalPreferences.allSavedAccounts(), - accountViewModel::okHttpClientForPushRegistration, + accountViewModel.httpClientBuilder::okHttpClientForPushRegistration, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt index 1102c7ec0..f06c49cff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt @@ -42,7 +42,6 @@ import androidx.compose.ui.Alignment.Companion.CenterStart import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route @@ -164,7 +163,7 @@ fun NormalChatNote( isLoggedInUser = isLoggedInUser, isDraft = note.event is DraftWrapEvent, innerQuote = innerQuote, - isComplete = accountViewModel.settings.featureSet == FeatureSetType.COMPLETE, + isComplete = accountViewModel.settings.isCompleteUIMode(), hasDetailsToShow = note.zaps.isNotEmpty() || note.zapPayments.isNotEmpty() || note.reactions.isNotEmpty(), drawAuthorInfo = drawAuthorInfo, parentBackgroundColor = parentBackgroundColor, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt index dd7f65e17..4f21b9fc8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt @@ -49,8 +49,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Constants -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer @@ -58,7 +58,6 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter @@ -160,7 +159,7 @@ fun RenderChannelData( contentDescription = stringRes(R.string.channel_image), modifier = MaterialTheme.colorScheme.largeProfilePictureModifier, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), ) } } @@ -237,7 +236,7 @@ fun RenderRelayLinePublicChat( nav: INav, ) { @Suppress("ProduceStateDoesNotAssignValue") - val relayInfo by loadRelayInfo(relay, accountViewModel) + val relayInfo by loadRelayInfo(relay) val clipboardManager = LocalClipboardManager.current val clickableModifier = @@ -255,7 +254,7 @@ fun RenderRelayLinePublicChat( relayInfo.icon, clickableModifier, showPicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index f28e07565..f5dcda623 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -667,7 +667,7 @@ class ChatNewMessageViewModel : viewModelScope.launch(Dispatchers.IO) { iMetaAttachments.downloadAndPrepare(item.link.url) { - Amethyst.instance.okHttpClients.getHttpClient(accountViewModel.account.privacyState.shouldUseTorForImageDownload(item.link.url)) + Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt index 8a1a85188..94e645d35 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header -import android.util.Log import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -28,9 +27,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.State import androidx.compose.runtime.getValue -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -38,9 +35,8 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.service.Nip11CachedRetriever +import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage @@ -51,28 +47,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemC import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier import com.vitorpamplona.amethyst.ui.theme.Size35dp -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation - -@Composable -fun loadRelayInfo( - relay: NormalizedRelayUrl, - accountViewModel: AccountViewModel, -): State = - produceState( - Nip11CachedRetriever.getFromCache(relay), - relay, - ) { - accountViewModel.retrieveRelayDocument( - relay = relay, - onInfo = { - value = it - }, - onError = { url, errorCode, exceptionMessage -> - Log.e("RelayInfo", "Error loading relay info for ${url.url}: $errorCode - $exceptionMessage") - }, - ) - } @Composable fun ShortEphemeralChatChannelHeader( @@ -119,16 +93,16 @@ private fun DrawRelayIcon( channel: EphemeralChatChannel, accountViewModel: AccountViewModel, ) { - val relayInfo by loadRelayInfo(channel.roomId.relayUrl, accountViewModel) + val relayInfo by loadRelayInfo(channel.roomId.relayUrl) RobohashFallbackAsyncImage( robot = channel.roomId.toKey(), - model = relayInfo?.icon, + model = relayInfo.icon, contentDescription = stringRes(R.string.profile_image), contentScale = ContentScale.Crop, modifier = HeaderPictureModifier, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt index 625232a29..23ee37873 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt @@ -41,7 +41,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel @@ -89,7 +88,7 @@ fun LongPublicChatChannelHeader( contentDescription = stringRes(R.string.channel_image), modifier = MaterialTheme.colorScheme.largeProfilePictureModifier, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt index e468db4a2..eb3d272b4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt @@ -37,7 +37,6 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel @@ -72,7 +71,7 @@ fun ShortPublicChatChannelHeader( contentScale = ContentScale.Crop, modifier = HeaderPictureModifier, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt index dc2b2d24e..4a827f595 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt @@ -210,7 +210,7 @@ class ChannelMetadataViewModel : ViewModel() { alt = null, sensitiveContent = null, serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, onProgress = {}, httpAuth = account::createHTTPAuthorization, context = context, @@ -223,7 +223,7 @@ class ChannelMetadataViewModel : ViewModel() { alt = null, sensitiveContent = null, serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, httpAuth = account::createBlossomUploadAuth, context = context, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt index 4d416e58a..0327d45f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt @@ -34,7 +34,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType @@ -74,7 +73,7 @@ fun ChannelFileUploadDialog( contentScale = ContentScale.Crop, modifier = HeaderPictureModifier, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 8eb6ba01d..41a6955cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -582,9 +582,7 @@ open class ChannelNewMessageViewModel : viewModelScope.launch(Dispatchers.IO) { iMetaAttachments.downloadAndPrepare(item.link.url) { - Amethyst.instance.okHttpClients.getHttpClient( - accountViewModel.account.privacyState.shouldUseTorForImageDownload(item.link.url), - ) + Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index 002642d94..935e2742a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -52,10 +52,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteHasEvent @@ -74,7 +74,6 @@ import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.RoomNameDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.LoadEphemeralChatChannel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.AccountPictureModifier import com.vitorpamplona.amethyst.ui.theme.Size55dp @@ -197,7 +196,7 @@ private fun ChannelRoomCompose( channelLastContent = "$authorName: $description", hasNewMessages = (noteEvent?.createdAt ?: Long.MIN_VALUE) > lastReadTime, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), onClick = { nav.nav(routeFor(channel)) }, ) } @@ -214,7 +213,7 @@ private fun ChannelRoomCompose( val channel = channelState?.channel as? EphemeralChatChannel ?: return - val relayInfo by loadRelayInfo(channel.roomId.relayUrl, accountViewModel) + val relayInfo by loadRelayInfo(channel.roomId.relayUrl) val noteEvent = lastMessage.event val description = noteEvent?.content?.take(200) @@ -229,7 +228,7 @@ private fun ChannelRoomCompose( channelLastContent = "$authorName: $description", hasNewMessages = (noteEvent?.createdAt ?: Long.MIN_VALUE) > lastReadTime, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), onClick = { nav.nav(routeFor(channel)) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index 8f37a64f6..4833ab0d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -541,10 +541,9 @@ open class NewProductViewModel : val wordToInsert = item.link.url + " " viewModelScope.launch(Dispatchers.IO) { - iMetaDescription.downloadAndPrepare( - item.link.url, - { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.privacyState?.shouldUseTorForImageDownload(item.link.url) ?: false) }, - ) + iMetaDescription.downloadAndPrepare(item.link.url) { + Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url) + } } message = message.replaceCurrentWord(wordToInsert) 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 3dc9a0cd6..5976ed234 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 @@ -815,12 +815,8 @@ open class ShortNotePostViewModel : val wordToInsert = item.link.url + " " viewModelScope.launch(Dispatchers.IO) { - iMetaAttachments.downloadAndPrepare( - item.link.url, - ) { - Amethyst.instance.okHttpClients.getHttpClient( - accountViewModel.account.privacyState.shouldUseTorForImageDownload(item.link.url), - ) + iMetaAttachments.downloadAndPrepare(item.link.url) { + Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt index 2127ba825..13c85120e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt @@ -20,9 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications -import android.Manifest -import android.os.Build -import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.padding @@ -31,29 +28,24 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.PermissionState -import com.google.accompanist.permissions.isGranted -import com.google.accompanist.permissions.rememberPermissionState +import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.ui.components.SelectNotificationProvider import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route -import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable fun NotificationScreen( - sharedPreferencesViewModel: SharedPreferencesViewModel, accountViewModel: AccountViewModel, nav: INav, ) { NotificationScreen( notifFeedContentState = accountViewModel.feedStates.notifications, notifSummaryState = accountViewModel.feedStates.notificationSummary, - sharedPreferencesViewModel = sharedPreferencesViewModel, + sharedPrefs = accountViewModel.settings.uiSettingsFlow, accountViewModel = accountViewModel, nav = nav, ) @@ -63,11 +55,11 @@ fun NotificationScreen( fun NotificationScreen( notifFeedContentState: CardFeedContentState, notifSummaryState: NotificationSummaryState, - sharedPreferencesViewModel: SharedPreferencesViewModel, + sharedPrefs: UiSettingsFlow, accountViewModel: AccountViewModel, nav: INav, ) { - SelectNotificationProvider(sharedPreferencesViewModel) + SelectNotificationProvider(sharedPrefs) WatchAccountForNotifications(notifFeedContentState, accountViewModel) @@ -106,29 +98,6 @@ fun NotificationScreen( } } -@RequiresApi(Build.VERSION_CODES.TIRAMISU) -@OptIn(ExperimentalPermissionsApi::class) -@Composable -fun checkifItNeedsToRequestNotificationPermission(sharedPreferencesViewModel: SharedPreferencesViewModel): PermissionState { - val notificationPermissionState = - rememberPermissionState( - Manifest.permission.POST_NOTIFICATIONS, - ) - - if (!sharedPreferencesViewModel.sharedPrefs.dontAskForNotificationPermissions) { - if (!notificationPermissionState.status.isGranted) { - sharedPreferencesViewModel.dontAskForNotificationPermissions() - - // This will pause the APP, including the connection with relays. - LaunchedEffect(notificationPermissionState) { - notificationPermissionState.launchPermissionRequest() - } - } - } - - return notificationPermissionState -} - @Composable fun WatchAccountForNotifications( notifFeedContentState: CardFeedContentState, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index d4091b487..5925e9f50 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -585,7 +585,7 @@ class NewPublicMessageViewModel : viewModelScope.launch(Dispatchers.IO) { iMetaAttachments.downloadAndPrepare(item.link.url) { - Amethyst.instance.okHttpClients.getHttpClient(accountViewModel.account.privacyState.shouldUseTorForImageDownload(item.link.url)) + Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt index 87179f8f4..2ea3f5fc3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt @@ -28,32 +28,47 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.preferences.AccountPreferenceStores.Companion.torSettings import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.tor.PrivacySettingsBody import com.vitorpamplona.amethyst.ui.tor.TorDialogViewModel +import com.vitorpamplona.amethyst.ui.tor.TorSettings +import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow @OptIn(ExperimentalMaterial3Api::class) @Composable fun PrivacyOptionsScreen( - accountViewModel: AccountViewModel, + torSettingsFlow: TorSettingsFlow, nav: INav, ) { val dialogViewModel = viewModel() - LaunchedEffect(dialogViewModel, accountViewModel) { - dialogViewModel.reset( - accountViewModel.account.settings.torSettings - .toSettings(), - ) - } + // runs only once and before the rest of the screen is build + // to avoid blinking and animations from the default/previous + // state to the current state + val init = + remember(dialogViewModel) { + val torSettings = torSettingsFlow.toSettings() + dialogViewModel.reset(torSettings) + torSettings + } + PrivacyOptionsScreenContents(dialogViewModel, onPost = torSettingsFlow::update, nav) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PrivacyOptionsScreenContents( + dialogViewModel: TorDialogViewModel, + onPost: (TorSettings) -> Unit, + nav: INav, +) { Scaffold( topBar = { SavingTopBar( @@ -62,7 +77,7 @@ fun PrivacyOptionsScreen( nav.popBack() }, onPost = { - accountViewModel.setTorSettings(dialogViewModel.save()) + onPost(dialogViewModel.save()) nav.popBack() }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt index f2296805c..315b86065 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt @@ -297,7 +297,7 @@ fun UrlVideoView( callbackUri = content.uri, mimeType = content.mimeType, aspectRatio = ratio, - proxyPort = accountViewModel.proxyPortForVideo(content.url), + proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(content.url), ) { mediaItem -> GetVideoController( mediaItem = mediaItem, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt index c4f8729cb..553d165ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt @@ -89,7 +89,7 @@ private fun GalleryFeedLoaded( val items by loaded.feed.collectAsStateWithLifecycle() val ratio = - if (accountViewModel.settings.modernGalleryStyle.value) { + if (accountViewModel.settings.modernGalleryStyle()) { 0.8f } else { 1.0f diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt index b72e07e54..b1ed91bd7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt @@ -34,7 +34,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -194,7 +193,7 @@ private fun RenderBadgeImage( robot = "badgenotfound", contentDescription = description, modifier = BadgePictureModifier, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), ) } else { RobohashFallbackAsyncImage( @@ -203,7 +202,7 @@ private fun RenderBadgeImage( contentDescription = description, modifier = BadgePictureModifier, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRDialog.kt index 98d79371e..76d2d7cf8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRDialog.kt @@ -55,7 +55,6 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.DisplayNIP05 @@ -166,7 +165,7 @@ fun ShowQRDialog( contentDescription = stringRes(R.string.profile_image), modifier = MaterialTheme.colorScheme.largeProfilePictureModifier, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), ) } Row( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt index 9746b5a66..bd112c836 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt @@ -51,7 +51,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.FeatureSetType +import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.ClickableEmail import com.vitorpamplona.amethyst.ui.components.ClickableUrl @@ -61,7 +61,6 @@ import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton import com.vitorpamplona.amethyst.ui.stringRes @@ -126,7 +125,7 @@ fun RelayInformationScreen( ) }, ) { pad -> - val relayInfo by loadRelayInfo(relay, accountViewModel) + val relayInfo by loadRelayInfo(relay) val messages = remember(relay) { @@ -158,7 +157,7 @@ fun RelayInformationScreen( displayUrl = relay.displayUrl(), iconUrl = relayInfo.icon, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), RelayStats.get(relay).pingInMs, iconModifier = LargeRelayIconModifier, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt index 2b1449936..5b7678a8a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt @@ -37,11 +37,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.AnnotatedString import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.HalfHalfVertPadding @@ -80,7 +80,7 @@ fun BasicRelaySetupInfoClickableRow( verticalAlignment = Alignment.CenterVertically, modifier = HalfVertPadding, ) { - val iconUrlFromRelayInfoDoc by loadRelayInfo(item.relay, accountViewModel) + val iconUrlFromRelayInfoDoc by loadRelayInfo(item.relay) RenderRelayIcon( iconUrlFromRelayInfoDoc.id ?: item.relay.displayUrl(), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt index 3d65385a3..12bd271a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -37,7 +36,7 @@ fun BasicRelaySetupInfoDialog( BasicRelaySetupInfoClickableRow( item = item, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), onDelete = onDelete, accountViewModel = accountViewModel, onClick = { nav.nav(Route.RelayInfo(item.relay.url)) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt index 89875455a..a24e7f683 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt @@ -24,7 +24,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.replace import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -69,11 +68,8 @@ abstract class BasicRelaySetupInfoModel : ViewModel() { fun loadRelayDocuments() { viewModelScope.launch(Dispatchers.IO) { _relays.value.forEach { item -> - Nip11CachedRetriever.loadRelayInfo( + Amethyst.instance.nip11Cache.loadRelayInfo( relay = item.relay, - okHttpClient = { - Amethyst.instance.okHttpClients.getHttpClient(account.torRelayState.shouldUseTorForClean(item.relay)) - }, onInfo = { togglePaidRelay(item, it.limitation?.payment_required ?: false) }, @@ -91,7 +87,7 @@ abstract class BasicRelaySetupInfoModel : ViewModel() { relaySetupInfoBuilder( normalized = it, forcesTor = - account.torRelayState.flow.value + Amethyst.instance.torEvaluatorFlow.flow.value .useTor(it), ) }.distinctBy { it.relay } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt index a76ebe9ee..4ededcdf4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel @@ -36,7 +37,7 @@ class ConnectedRelayListViewModel : BasicRelaySetupInfoModel() { relay = it, relayStat = RelayStats.get(it), forcesTor = - account.torRelayState.flow.value + Amethyst.instance.torEvaluatorFlow.flow.value .useTor(it), users = account.followsPerRelay.value[it]?.mapNotNull { hex -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt index c2d82c8c8..8999830e2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt @@ -25,7 +25,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.replace import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo @@ -91,9 +90,8 @@ class Nip65RelayListViewModel : ViewModel() { fun loadRelayDocuments() { viewModelScope.launch(Dispatchers.IO) { _homeRelays.value.forEach { item -> - Nip11CachedRetriever.loadRelayInfo( + Amethyst.instance.nip11Cache.loadRelayInfo( relay = item.relay, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.torRelayState.shouldUseTorForClean(item.relay)) }, onInfo = { toggleHomePaidRelay(item, it.limitation?.payment_required ?: false) }, @@ -102,9 +100,8 @@ class Nip65RelayListViewModel : ViewModel() { } _notificationRelays.value.forEach { item -> - Nip11CachedRetriever.loadRelayInfo( + Amethyst.instance.nip11Cache.loadRelayInfo( relay = item.relay, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.torRelayState.shouldUseTorForClean(item.relay)) }, onInfo = { toggleNotifPaidRelay(item, it.limitation?.payment_required ?: false) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt index adc9244d6..7bfd3dc09 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt @@ -55,8 +55,8 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.service.relayClient.searchCommand.TextSearchDataSourceSubscription import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold @@ -69,7 +69,6 @@ import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.SearchIcon import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChannelName import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness @@ -294,7 +293,7 @@ private fun DisplaySearchResults( channelLastContent = item.summary(), hasNewMessages = false, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), onClick = { nav.nav(routeFor(item)) }, ) @@ -308,7 +307,7 @@ private fun DisplaySearchResults( ephemeralChannels, key = { _, item -> "ephem" + item.roomId.toKey() }, ) { _, item -> - val relayInfo by loadRelayInfo(item.roomId.relayUrl, accountViewModel) + val relayInfo by loadRelayInfo(item.roomId.relayUrl) ChannelName( channelIdHex = item.roomId.toKey(), @@ -323,7 +322,7 @@ private fun DisplaySearchResults( channelLastContent = stringRes(R.string.ephemeral_relay_chat), hasNewMessages = false, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), onClick = { nav.nav(routeFor(item)) }, ) @@ -350,7 +349,7 @@ private fun DisplaySearchResults( channelLastContent = item.summary(), hasNewMessages = false, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + loadRobohash = accountViewModel.settings.isNotPerformanceMode(), onClick = { nav.nav(routeFor(item)) }, ) 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 0248eecc0..2e448ab7d 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 @@ -24,7 +24,6 @@ import android.content.Context import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -35,6 +34,8 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -50,6 +51,7 @@ import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.ProfileGalleryType import com.vitorpamplona.amethyst.model.ThemeType +import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.model.parseBooleanType import com.vitorpamplona.amethyst.model.parseConnectivityType import com.vitorpamplona.amethyst.model.parseFeatureSetType @@ -61,11 +63,9 @@ import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton -import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.mockSharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer +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 @@ -78,6 +78,55 @@ import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserException import java.io.IOException +@Composable +fun SettingsScreen( + accountViewModel: AccountViewModel, + nav: INav, +) { + DisappearingScaffold( + isInvertedLayout = false, + topBar = { + TopBarWithBackButton(stringRes(id = R.string.application_preferences), nav::popBack) + }, + accountViewModel = accountViewModel, + ) { + Column(Modifier.padding(it)) { + SettingsScreen(accountViewModel.settings.uiSettingsFlow) + } + } +} + +@Preview(device = "spec:width=2160px,height=2340px,dpi=440") +@Composable +fun SettingsScreenPreview() { + ThemeComparisonRow { + SettingsScreen(UiSettingsFlow()) + } +} + +@Composable +fun SettingsScreen(sharedPrefs: UiSettingsFlow) { + Column( + Modifier + .fillMaxSize() + .padding(top = Size10dp, start = Size20dp, end = Size20dp) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = RowColSpacing, + ) { + ShowLanguageChoice(sharedPrefs) + ShowThemeChoice(sharedPrefs) + ShowImagePreviewChoice(sharedPrefs) + ShowVideoPlaybackChoice(sharedPrefs) + ShowUrlPreviewChoice(sharedPrefs) + ShowProfilePictureChoice(sharedPrefs) + ImmersiveScrollingChoice(sharedPrefs) + FeatureSetChoice(sharedPrefs) + GalleryChoice(sharedPrefs) + PushNotificationSettingsRow(sharedPrefs) + } +} + fun Context.getLocaleListFromXml(): LocaleListCompat { val tagsList = mutableListOf() try { @@ -116,9 +165,8 @@ fun Context.getLangPreferenceDropdownEntries(): ImmutableMap { fun getLanguageIndex( languageEntries: ImmutableMap, - sharedPreferencesViewModel: SharedPreferencesViewModel, + language: String?, ): Int { - val language = sharedPreferencesViewModel.sharedPrefs.language var languageIndex: Int languageIndex = if (language != null) { @@ -134,55 +182,153 @@ fun getLanguageIndex( } @Composable -fun SettingsScreen( - sharedPreferencesViewModel: SharedPreferencesViewModel, - accountViewModel: AccountViewModel, - nav: INav, -) { - DisappearingScaffold( - isInvertedLayout = false, - topBar = { - TopBarWithBackButton(stringRes(id = R.string.application_preferences), nav::popBack) - }, - accountViewModel = accountViewModel, +fun ShowLanguageChoice(sharedPrefs: UiSettingsFlow) { + val context = LocalContext.current + + val languageEntries = remember { context.getLangPreferenceDropdownEntries() } + val languageList = remember { languageEntries.keys.map { TitleExplainer(it) }.toImmutableList() } + + val language by sharedPrefs.preferredLanguage.collectAsState() + + val languageIndex = getLanguageIndex(languageEntries, language) + + SettingsRow( + R.string.language, + R.string.language_description, + languageList, + languageIndex, ) { - Column(Modifier.padding(it)) { - SettingsScreen(sharedPreferencesViewModel) - } - } -} - -@Preview(device = "spec:width=2160px,height=2340px,dpi=440") -@Composable -fun SettingsScreenPreview() { - val sharedPreferencesViewModel = mockSharedPreferencesViewModel() - ThemeComparisonRow { - SettingsScreen(sharedPreferencesViewModel) + sharedPrefs.preferredLanguage.tryEmit(languageEntries[languageList[it].title]) } } @Composable -fun SettingsScreen(sharedPreferencesViewModel: SharedPreferencesViewModel) { - val selectedItens = +fun ShowThemeChoice(sharedPrefs: UiSettingsFlow) { + val themeOptions = + persistentListOf( + TitleExplainer(stringRes(ThemeType.SYSTEM.resourceId)), + TitleExplainer(stringRes(ThemeType.LIGHT.resourceId)), + TitleExplainer(stringRes(ThemeType.DARK.resourceId)), + ) + + val themeIndex by sharedPrefs.theme.collectAsState() + + SettingsRow( + R.string.theme, + R.string.theme_description, + themeOptions, + themeIndex.screenCode, + ) { + sharedPrefs.theme.tryEmit(parseThemeType(it)) + } +} + +@Composable +fun ShowImagePreviewChoice(sharedPrefs: UiSettingsFlow) { + val connectivityBasedOptions = persistentListOf( TitleExplainer(stringRes(ConnectivityType.ALWAYS.resourceId)), TitleExplainer(stringRes(ConnectivityType.WIFI_ONLY.resourceId)), TitleExplainer(stringRes(ConnectivityType.NEVER.resourceId)), ) - val themeItens = + val showImagesIndex by sharedPrefs.automaticallyShowImages.collectAsState() + + SettingsRow( + R.string.automatically_load_images_gifs, + R.string.automatically_load_images_gifs_description, + connectivityBasedOptions, + showImagesIndex.screenCode, + ) { + sharedPrefs.automaticallyShowImages.tryEmit(parseConnectivityType(it)) + } +} + +@Composable +fun ShowVideoPlaybackChoice(sharedPrefs: UiSettingsFlow) { + val connectivityBasedOptions = persistentListOf( - TitleExplainer(stringRes(ThemeType.SYSTEM.resourceId)), - TitleExplainer(stringRes(ThemeType.LIGHT.resourceId)), - TitleExplainer(stringRes(ThemeType.DARK.resourceId)), + TitleExplainer(stringRes(ConnectivityType.ALWAYS.resourceId)), + TitleExplainer(stringRes(ConnectivityType.WIFI_ONLY.resourceId)), + TitleExplainer(stringRes(ConnectivityType.NEVER.resourceId)), ) + val videoIndex by sharedPrefs.automaticallyStartPlayback.collectAsState() + + SettingsRow( + R.string.automatically_play_videos, + R.string.automatically_play_videos_description, + connectivityBasedOptions, + videoIndex.screenCode, + ) { + sharedPrefs.automaticallyStartPlayback.tryEmit(parseConnectivityType(it)) + } +} + +@Composable +fun ShowUrlPreviewChoice(sharedPrefs: UiSettingsFlow) { + val connectivityBasedOptions = + persistentListOf( + TitleExplainer(stringRes(ConnectivityType.ALWAYS.resourceId)), + TitleExplainer(stringRes(ConnectivityType.WIFI_ONLY.resourceId)), + TitleExplainer(stringRes(ConnectivityType.NEVER.resourceId)), + ) + + val linkIndex by sharedPrefs.automaticallyShowUrlPreview.collectAsState() + + SettingsRow( + R.string.automatically_show_url_preview, + R.string.automatically_show_url_preview_description, + connectivityBasedOptions, + linkIndex.screenCode, + ) { + sharedPrefs.automaticallyShowUrlPreview.tryEmit(parseConnectivityType(it)) + } +} + +@Composable +fun ShowProfilePictureChoice(sharedPrefs: UiSettingsFlow) { + val profilePictureIndex by sharedPrefs.automaticallyShowProfilePictures.collectAsState() + + val connectivityBasedOptions = + persistentListOf( + TitleExplainer(stringRes(ConnectivityType.ALWAYS.resourceId)), + TitleExplainer(stringRes(ConnectivityType.WIFI_ONLY.resourceId)), + TitleExplainer(stringRes(ConnectivityType.NEVER.resourceId)), + ) + + SettingsRow( + R.string.automatically_show_profile_picture, + R.string.automatically_show_profile_picture_description, + connectivityBasedOptions, + profilePictureIndex.screenCode, + ) { + sharedPrefs.automaticallyShowProfilePictures.tryEmit(parseConnectivityType(it)) + } +} + +@Composable +fun ImmersiveScrollingChoice(sharedPrefs: UiSettingsFlow) { + val hideNavBarsIndex by sharedPrefs.automaticallyHideNavigationBars.collectAsState() + val booleanItems = persistentListOf( TitleExplainer(stringRes(ConnectivityType.ALWAYS.resourceId)), TitleExplainer(stringRes(ConnectivityType.NEVER.resourceId)), ) + SettingsRow( + R.string.automatically_hide_nav_bars, + R.string.automatically_hide_nav_bars_description, + booleanItems, + hideNavBarsIndex.screenCode, + ) { + sharedPrefs.automaticallyHideNavigationBars.tryEmit(parseBooleanType(it)) + } +} + +@Composable +fun FeatureSetChoice(sharedPrefs: UiSettingsFlow) { val featureItems = persistentListOf( TitleExplainer(stringRes(FeatureSetType.COMPLETE.resourceId)), @@ -190,136 +336,35 @@ fun SettingsScreen(sharedPreferencesViewModel: SharedPreferencesViewModel) { TitleExplainer(stringRes(FeatureSetType.PERFORMANCE.resourceId)), ) + val featureSetIndex by sharedPrefs.featureSet.collectAsState() + + SettingsRow( + R.string.ui_style, + R.string.ui_style_description, + featureItems, + featureSetIndex.screenCode, + ) { + sharedPrefs.featureSet.tryEmit(parseFeatureSetType(it)) + } +} + +@Composable +fun GalleryChoice(sharedPrefs: UiSettingsFlow) { val galleryItems = persistentListOf( TitleExplainer(stringRes(ProfileGalleryType.CLASSIC.resourceId)), TitleExplainer(stringRes(ProfileGalleryType.MODERN.resourceId)), ) - val showImagesIndex = sharedPreferencesViewModel.sharedPrefs.automaticallyShowImages.screenCode - val videoIndex = sharedPreferencesViewModel.sharedPrefs.automaticallyStartPlayback.screenCode - val linkIndex = sharedPreferencesViewModel.sharedPrefs.automaticallyShowUrlPreview.screenCode - val hideNavBarsIndex = - sharedPreferencesViewModel.sharedPrefs.automaticallyHideNavigationBars.screenCode - val profilePictureIndex = - sharedPreferencesViewModel.sharedPrefs.automaticallyShowProfilePictures.screenCode - val themeIndex = sharedPreferencesViewModel.sharedPrefs.theme.screenCode + val galleryIndex by sharedPrefs.gallerySet.collectAsState() - val context = LocalContext.current - - val languageEntries = remember { context.getLangPreferenceDropdownEntries() } - val languageList = remember { languageEntries.keys.map { TitleExplainer(it) }.toImmutableList() } - val languageIndex = getLanguageIndex(languageEntries, sharedPreferencesViewModel) - - val featureSetIndex = - sharedPreferencesViewModel.sharedPrefs.featureSet.screenCode - val galleryIndex = - sharedPreferencesViewModel.sharedPrefs.gallerySet.screenCode - - Column( - Modifier - .fillMaxSize() - .padding(top = Size10dp, start = Size20dp, end = Size20dp) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, + SettingsRow( + R.string.gallery_style, + R.string.gallery_style_description, + galleryItems, + galleryIndex.screenCode, ) { - SettingsRow( - R.string.language, - R.string.language_description, - languageList, - languageIndex, - ) { - sharedPreferencesViewModel.updateLanguage(languageEntries[languageList[it].title]) - } - - Spacer(modifier = HalfVertSpacer) - - SettingsRow( - R.string.theme, - R.string.theme_description, - themeItens, - themeIndex, - ) { - sharedPreferencesViewModel.updateTheme(parseThemeType(it)) - } - - Spacer(modifier = HalfVertSpacer) - - SettingsRow( - R.string.automatically_load_images_gifs, - R.string.automatically_load_images_gifs_description, - selectedItens, - showImagesIndex, - ) { - sharedPreferencesViewModel.updateAutomaticallyShowImages(parseConnectivityType(it)) - } - - Spacer(modifier = HalfVertSpacer) - - SettingsRow( - R.string.automatically_play_videos, - R.string.automatically_play_videos_description, - selectedItens, - videoIndex, - ) { - sharedPreferencesViewModel.updateAutomaticallyStartPlayback(parseConnectivityType(it)) - } - - Spacer(modifier = HalfVertSpacer) - - SettingsRow( - R.string.automatically_show_url_preview, - R.string.automatically_show_url_preview_description, - selectedItens, - linkIndex, - ) { - sharedPreferencesViewModel.updateAutomaticallyShowUrlPreview(parseConnectivityType(it)) - } - - SettingsRow( - R.string.automatically_show_profile_picture, - R.string.automatically_show_profile_picture_description, - selectedItens, - profilePictureIndex, - ) { - sharedPreferencesViewModel.updateAutomaticallyShowProfilePicture(parseConnectivityType(it)) - } - - Spacer(modifier = HalfVertSpacer) - - SettingsRow( - R.string.automatically_hide_nav_bars, - R.string.automatically_hide_nav_bars_description, - booleanItems, - hideNavBarsIndex, - ) { - sharedPreferencesViewModel.updateAutomaticallyHideNavBars(parseBooleanType(it)) - } - - Spacer(modifier = HalfVertSpacer) - - SettingsRow( - R.string.ui_style, - R.string.ui_style_description, - featureItems, - featureSetIndex, - ) { - sharedPreferencesViewModel.updateFeatureSetType(parseFeatureSetType(it)) - } - Spacer(modifier = HalfVertSpacer) - - Spacer(modifier = HalfVertSpacer) - - SettingsRow( - R.string.gallery_style, - R.string.gallery_style_description, - galleryItems, - galleryIndex, - ) { - sharedPreferencesViewModel.updateGallerySetType(parseGalleryType(it)) - } - - PushNotificationSettingsRow(sharedPreferencesViewModel) + sharedPrefs.gallerySet.tryEmit(parseGalleryType(it)) } } @@ -327,15 +372,15 @@ fun SettingsScreen(sharedPreferencesViewModel: SharedPreferencesViewModel) { fun SettingsRow( name: Int, description: Int, - selectedItens: ImmutableList, + selectedItems: ImmutableList, selectedIndex: Int, onSelect: (Int) -> Unit, ) { SettingsRow(name, description) { TextSpinner( label = "", - placeholder = selectedItens[selectedIndex].title, - options = selectedItens, + placeholder = selectedItems[selectedIndex].title, + options = selectedItems, onSelect = onSelect, modifier = Modifier.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt index 6628b659a..7053278c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt @@ -51,7 +51,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.ClickableBox @@ -327,7 +326,7 @@ private fun RenderAuthorInformation( NoteUsernameDisplay(note, Modifier.weight(1f), accountViewModel = accountViewModel) VideoUserOptionAction(note, accountViewModel, nav) } - if (accountViewModel.settings.featureSet == FeatureSetType.COMPLETE) { + if (accountViewModel.settings.isCompleteUIMode()) { Row(verticalAlignment = Alignment.CenterVertically) { ObserveDisplayNip05Status( note.author!!, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/TorSettingsSetup.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/TorSettingsSetup.kt index 9e3ca242b..c991b54df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/TorSettingsSetup.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/TorSettingsSetup.kt @@ -32,15 +32,15 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.preferences.AccountPreferenceStores.Companion.torSettings import com.vitorpamplona.amethyst.ui.components.appendLink import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.tor.ConnectTorDialog -import com.vitorpamplona.amethyst.ui.tor.TorSettings +import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow @Composable fun TorSettingsSetup( - torSettings: TorSettings, - onCheckedChange: (TorSettings) -> Unit, + torSettingsFlow: TorSettingsFlow, onError: (String) -> Unit, ) { var connectOrbotDialogOpen by remember { mutableStateOf(false) } @@ -57,11 +57,11 @@ fun TorSettingsSetup( if (connectOrbotDialogOpen) { ConnectTorDialog( - torSettings = torSettings, + torSettings = torSettingsFlow.toSettings(), onClose = { connectOrbotDialogOpen = false }, onPost = { torSettings -> connectOrbotDialogOpen = false - onCheckedChange(torSettings) + torSettingsFlow.update(torSettings) }, onError = onError, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginScreen.kt index 1eab153c2..29222c2ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginScreen.kt @@ -74,6 +74,7 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.hashtags.Amethyst import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons @@ -170,15 +171,12 @@ fun LoginPage( } } - Spacer(modifier = Modifier.height(10.dp)) - PasswordField(loginViewModel) Spacer(modifier = Modifier.height(10.dp)) TorSettingsSetup( - torSettings = loginViewModel.torSettings, - onCheckedChange = loginViewModel::updateTorSettings, + torSettingsFlow = Amethyst.instance.torPrefs.value, onError = { scope.launch { Toast @@ -262,6 +260,8 @@ fun OfferTemporaryAccount( @Composable private fun PasswordField(loginViewModel: LoginViewModel) { if (loginViewModel.needsPassword) { + Spacer(modifier = Modifier.height(10.dp)) + val passwordFocusRequester = remember { FocusRequester() } PasswordField( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel.kt index eab3f7b75..50cbcd0be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel.kt @@ -28,7 +28,6 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel -import com.vitorpamplona.amethyst.ui.tor.TorSettings class LoginViewModel : ViewModel() { lateinit var accountStateViewModel: AccountStateViewModel @@ -39,7 +38,6 @@ class LoginViewModel : ViewModel() { var acceptedTerms by mutableStateOf(false) var termsAcceptanceIsRequiredError by mutableStateOf(false) - var torSettings by mutableStateOf(TorSettings()) var offerTemporaryLogin by mutableStateOf(false) var isTemporary by mutableStateOf(false) @@ -77,7 +75,6 @@ class LoginViewModel : ViewModel() { processingLogin = false isTemporary = false offerTemporaryLogin = false - torSettings = TorSettings() isFirstLogin = false } @@ -98,10 +95,6 @@ class LoginViewModel : ViewModel() { errorManager.clearErrors() } - fun updateTorSettings(newTorSettings: TorSettings) { - torSettings = newTorSettings - } - fun updateAcceptedTerms(newAcceptedTerms: Boolean) { acceptedTerms = newAcceptedTerms if (newAcceptedTerms) { @@ -140,7 +133,6 @@ class LoginViewModel : ViewModel() { accountStateViewModel.login( key = key.text, password = password.text, - torSettings = torSettings, transientAccount = isTemporary, ) { processingLogin = false @@ -158,7 +150,6 @@ class LoginViewModel : ViewModel() { processingLogin = true accountStateViewModel.login( key = key.text, - torSettings = torSettings, transientAccount = isTemporary, loginWithExternalSigner = true, packageName = packageName, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpScreen.kt index cbb8f530f..20e73bed3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpScreen.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.hashtags.Amethyst import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons @@ -165,6 +166,20 @@ fun SignUpPage( Spacer(modifier = Modifier.height(10.dp)) + TorSettingsSetup( + torSettingsFlow = Amethyst.instance.torPrefs.value, + onError = { + scope.launch { + Toast + .makeText( + context, + it, + Toast.LENGTH_LONG, + ).show() + } + }, + ) + AcceptTerms( checked = signUpViewModel.acceptedTerms, onCheckedChange = signUpViewModel::updateAcceptedTerms, @@ -178,21 +193,6 @@ fun SignUpPage( ) } - TorSettingsSetup( - torSettings = signUpViewModel.torSettings, - onCheckedChange = signUpViewModel::updateTorSettings, - onError = { - scope.launch { - Toast - .makeText( - context, - it, - Toast.LENGTH_LONG, - ).show() - } - }, - ) - Spacer(modifier = Modifier.height(Size10dp)) Box(modifier = Modifier.padding(Size40dp, 0.dp, Size40dp, 0.dp)) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpViewModel.kt index 39f8500a3..02e29f261 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpViewModel.kt @@ -28,7 +28,6 @@ import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.loggedOff.login.LoginErrorManager -import com.vitorpamplona.amethyst.ui.tor.TorSettings class SignUpViewModel : ViewModel() { lateinit var accountStateViewModel: AccountStateViewModel @@ -40,8 +39,6 @@ class SignUpViewModel : ViewModel() { var acceptedTerms by mutableStateOf(false) var termsAcceptanceIsRequiredError by mutableStateOf(false) - var torSettings by mutableStateOf(TorSettings()) - fun init(accountStateViewModel: AccountStateViewModel) { this.accountStateViewModel = accountStateViewModel } @@ -51,10 +48,6 @@ class SignUpViewModel : ViewModel() { errorManager.clearErrors() } - fun updateTorSettings(newTorSettings: TorSettings) { - torSettings = newTorSettings - } - fun updateAcceptedTerms(newAcceptedTerms: Boolean) { acceptedTerms = newAcceptedTerms if (newAcceptedTerms) { @@ -80,7 +73,7 @@ class SignUpViewModel : ViewModel() { fun signup() { if (checkCanSignup()) { - accountStateViewModel.newKey(torSettings, displayName.text) + accountStateViewModel.newKey(displayName.text) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 22ee9aa21..29a1e50a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -54,10 +54,12 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.view.WindowCompat +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.halilibo.richtext.ui.RichTextStyle import com.halilibo.richtext.ui.resolveDefaults import com.patrykandpatrick.vico.compose.common.VicoTheme import com.patrykandpatrick.vico.compose.common.VicoTheme.CandlestickCartesianLayerColors +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.ThemeType private val DarkColorPalette = @@ -482,6 +484,14 @@ val chartDarkColors = val ColorScheme.chartStyle: VicoTheme get() = if (isLight) chartLightColors else chartDarkColors +@Composable +fun AmethystTheme(content: @Composable () -> Unit) { + val theme by Amethyst.instance.uiPrefs.value.theme + .collectAsStateWithLifecycle() + + AmethystTheme(theme, content) +} + @Composable fun AmethystTheme( prefTheme: ThemeType, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorManager.kt index 9b0169c6b..c6de327b3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorManager.kt @@ -20,12 +20,19 @@ */ package com.vitorpamplona.amethyst.ui.tor -import android.app.Application +import android.content.Context +import com.vitorpamplona.amethyst.model.preferences.TorSharedPreferences import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest /** * There should be only one instance of the Tor binding per app. @@ -33,15 +40,41 @@ import kotlinx.coroutines.flow.stateIn * Tor will connect as soon as status is listened to. */ class TorManager( - app: Application, + torPrefs: TorSharedPreferences, + app: Context, scope: CoroutineScope, ) { - val status: StateFlow = - TorService(app).status.stateIn( - scope, - SharingStarted.WhileSubscribed(30000), - TorServiceStatus.Off, - ) + val service = TorService(app) + + @OptIn(ExperimentalCoroutinesApi::class) + val status = + combine( + torPrefs.value.torType, + torPrefs.value.externalSocksPort, + ) { torType, externalSocksPort -> + Pair(torType, externalSocksPort) + }.transformLatest { (torType, externalSocksPort) -> + when (torType) { + TorType.INTERNAL -> { + emitAll(service.status) + } + TorType.OFF -> { + emit(TorServiceStatus.Off) + } + TorType.EXTERNAL -> { + if (externalSocksPort > 0) { + emit(TorServiceStatus.Active(externalSocksPort)) + } else { + emitAll(service.status) + } + } + } + }.flowOn(Dispatchers.IO) + .stateIn( + scope, + SharingStarted.WhileSubscribed(30000), + TorServiceStatus.Off, + ) val activePortOrNull: StateFlow = status diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettings.kt index 11edf8e83..b9e3744b2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettings.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.tor import com.vitorpamplona.amethyst.R -class TorSettings( +data class TorSettings( val torType: TorType = TorType.INTERNAL, val externalSocksPort: Int = 9050, val onionRelaysViaTor: Boolean = true, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsFlow.kt index d4d765d26..dc70e1d9e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsFlow.kt @@ -94,6 +94,8 @@ class TorSettingsFlow( fun update(torSettings: TorSettings): Boolean { var any = false + println("AABBCC updating 1 $torSettings") + if (torType.value != torSettings.torType) { torType.tryEmit(torSettings.torType) any = true @@ -148,6 +150,8 @@ class TorSettingsFlow( any = true } + println("AABBCC updating 2 ${toSettings()}") + return any } diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt index 564070521..ac47b0bcc 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt @@ -20,19 +20,36 @@ */ package com.vitorpamplona.amethyst.ui.components +import android.Manifest import android.os.Build import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.checkifItNeedsToRequestNotificationPermission +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.vitorpamplona.amethyst.model.UiSettingsFlow @OptIn(ExperimentalPermissionsApi::class) @Composable -fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesViewModel) { +fun SelectNotificationProvider(sharedPrefs: UiSettingsFlow) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - checkifItNeedsToRequestNotificationPermission(sharedPreferencesViewModel) + val notificationPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) + if (!notificationPermissionState.status.isGranted) { + val dontAskForNotificationPermissions by sharedPrefs.dontAskForNotificationPermissions.collectAsStateWithLifecycle() + + if (!dontAskForNotificationPermissions) { + sharedPrefs.dontAskForNotificationPermissions() + + // This will pause the APP, including the connection with relays. + LaunchedEffect(notificationPermissionState) { + notificationPermissionState.launchPermissionRequest() + } + } + } } } @Composable -fun PushNotificationSettingsRow(sharedPreferencesViewModel: SharedPreferencesViewModel) {} +fun PushNotificationSettingsRow(sharedPrefs: UiSettingsFlow) {} diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt index 54c1f7e5e..f8cb5a692 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt @@ -459,7 +459,7 @@ open class BasicRelayClient( ) } socket?.let { - // Log.d(logTag, "Sending: $str") + Log.d(logTag, "Sending: $str") val result = it.send(str) listener.onSend(this@BasicRelayClient, str, result) stats.addBytesSent(str.bytesUsedInMemory()) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt index 3e59d83bd..09b5b91e5 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt @@ -24,35 +24,31 @@ import android.util.LruCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.utils.TimeUtils -class VerificationStateCache { +class VerificationStateCache( + val otsResolverBuilder: OtsResolverBuilder, +) { private val cache = LruCache(200) - fun verify( - event: OtsEvent, - resolverBuilder: OtsResolverBuilder, - ): VerificationState { + fun verify(event: OtsEvent): VerificationState { cache.put(event.id, VerificationState.Verifying) - return event.verifyState(resolverBuilder.build()).also { cache.put(event.id, it) } + return event.verifyState(otsResolverBuilder.build()).also { cache.put(event.id, it) } } - fun cacheVerify( - event: OtsEvent, - resolverBuilder: OtsResolverBuilder, - ): VerificationState = + fun cacheVerify(event: OtsEvent): VerificationState = when (val verif = cache[event.id]) { is VerificationState.Verifying -> verif is VerificationState.Verified -> verif is VerificationState.NetworkError -> { // try again in 5 mins if (verif.time < TimeUtils.fiveMinutesAgo()) { - event.verifyState(resolverBuilder.build()).also { cache.put(event.id, it) } + event.verifyState(otsResolverBuilder.build()).also { cache.put(event.id, it) } } else { - verify(event, resolverBuilder) + verify(event) } } is VerificationState.Error -> verif else -> { - verify(event, resolverBuilder) + verify(event) } } }