diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt index f1d881ce5..99db5b628 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt @@ -31,7 +31,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal.ThreadFeedFi import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.verify diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt index 8f832704e..128c69420 100644 --- a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt +++ b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt @@ -53,7 +53,7 @@ object PushDistributorHandler : PushDistributorActions { endpointInternal = "" } - fun appContext(): Context = Amethyst.instance.applicationContext + fun appContext(): Context = Amethyst.instance.appContext override fun getSavedDistributor(): String = unifiedPush.getSavedDistributor(appContext()) ?: "" diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt index eed9a8c81..1d54873fb 100644 --- a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt +++ b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt @@ -27,8 +27,6 @@ import android.util.LruCache import androidx.core.content.ContextCompat import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences -import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateDMChannel -import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateZapChannel import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import kotlinx.coroutines.CancellationException @@ -44,7 +42,7 @@ class PushMessageReceiver : MessagingReceiver() { private val TAG = "Amethyst-OSSPushReceiver" } - private val appContext = Amethyst.instance.applicationContext + private val appContext = Amethyst.instance.appContext private val scope = Amethyst.instance.applicationIOScope private val eventCache = LruCache(100) private val pushHandler = PushDistributorHandler @@ -95,8 +93,8 @@ class PushMessageReceiver : MessagingReceiver() { PushNotificationUtils.checkAndInit(sanitizedEndpoint, LocalPreferences.allSavedAccounts()) { Amethyst.instance.okHttpClients.getHttpClient(Amethyst.instance.torManager.isSocksReady()) } - notificationManager().getOrCreateZapChannel(appContext) - notificationManager().getOrCreateDMChannel(appContext) + NotificationUtils.getOrCreateZapChannel(appContext) + NotificationUtils.getOrCreateDMChannel(appContext) } } else { Log.d(TAG, "Same endpoint provided:- $endpoint for Instance: $instance $sanitizedEndpoint") 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..0168e90a1 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,19 +210,19 @@ fun LoadDistributors(onInner: @Composable (String, ImmutableList, Immuta } @Composable -fun PushNotificationSettingsRow(sharedPreferencesViewModel: SharedPreferencesViewModel) { +fun PushNotificationSettingsRow(sharedPrefs: UiSettingsFlow) { val context = LocalContext.current LoadDistributors { currentDistributor, list, readableListWithExplainer -> SettingsRow( R.string.push_server_title, R.string.push_server_explainer, - selectedItens = readableListWithExplainer, + selectedItems = readableListWithExplainer, 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 d5b850e1c..7ae55fbab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -21,198 +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.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.tor.TorManager -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: NostrClient = 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) - - // 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") - - Thread.setDefaultUncaughtExceptionHandler(UnexpectedCrashSaver(crashReportCache, applicationIOScope)) - - instance = this + 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. * @@ -221,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 c424f3938..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 @@ -498,10 +494,10 @@ object LocalPreferences { Log.d("LocalPreferences", "Saved to encrypted storage") } - suspend fun loadCurrentAccountFromEncryptedStorage(): AccountSettings? = currentAccount()?.let { loadCurrentAccountFromEncryptedStorage(it) } + 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( @@ -529,7 +527,7 @@ object LocalPreferences { val mutex = Mutex() - suspend fun loadCurrentAccountFromEncryptedStorage(npub: String): AccountSettings? { + suspend fun loadAccountConfigFromEncryptedStorage(npub: String): AccountSettings? { // if already loaded, return right away if (cachedAccounts.containsKey(npub)) { return cachedAccounts[npub] @@ -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 09490b48d..8d6008b88 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -22,9 +22,10 @@ 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 +import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListDecryptionCache import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel @@ -78,18 +79,21 @@ 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.MergedFollowPlusMineWithIndexAndSearchRelayListsState +import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineWithIndexRelayListsState +import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineWithSearchRelayListsState import com.vitorpamplona.amethyst.model.serverList.MergedServerListState import com.vitorpamplona.amethyst.model.serverList.TrustedRelayListsState 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 +import com.vitorpamplona.amethyst.ui.screen.loggedIn.EventProcessor import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSet import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent @@ -122,7 +126,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.downloadFirstEvent import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter @@ -133,7 +137,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.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 @@ -207,6 +211,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -220,9 +225,11 @@ import kotlin.coroutines.cancellation.CancellationException class Account( val settings: AccountSettings = AccountSettings(KeyPair()), val signer: NostrSigner, - geolocationFlow: StateFlow, + val geolocationFlow: StateFlow, + val nwcFilterAssembler: NWCPaymentFilterAssembler, + val otsResolverBuilder: OtsResolverBuilder, val cache: LocalCache, - val client: NostrClient, + val client: INostrClient, val scope: CoroutineScope, ) { private var userProfileCache: User? = null @@ -231,7 +238,7 @@ class Account( val userMetadata = UserMetadataState(signer, cache, scope, settings) - val nip47SignerState = NwcSignerState(signer, cache, scope, settings) + val nip47SignerState = NwcSignerState(signer, nwcFilterAssembler, cache, scope, settings) val nip65RelayList = Nip65RelayListState(signer, cache, scope, settings) val localRelayList = LocalRelayListState(signer, cache, scope, settings) @@ -303,7 +310,10 @@ class Account( // Follows Relays val followOutboxesOrProxy = FollowListOutboxOrProxyRelays(kind3FollowList, blockedRelayList, proxyRelayList, cache, scope) - val followPlusAllMine = MergedFollowPlusMineRelayListsState(followOutboxesOrProxy, nip65RelayList, privateStorageRelayList, localRelayList, indexerRelayList, scope) + val followPlusAllMineWithIndex = MergedFollowPlusMineWithIndexRelayListsState(followOutboxesOrProxy, nip65RelayList, privateStorageRelayList, localRelayList, indexerRelayList, scope) + val followPlusAllMineWithSearch = MergedFollowPlusMineWithSearchRelayListsState(followOutboxesOrProxy, nip65RelayList, privateStorageRelayList, localRelayList, searchRelayList, scope) + val followPlusAllMineWithIndexAndSearch = MergedFollowPlusMineWithIndexAndSearchRelayListsState(followOutboxesOrProxy, nip65RelayList, privateStorageRelayList, localRelayList, indexerRelayList, searchRelayList, scope) + val defaultGlobalRelays = MergedFollowPlusMineRelayListsState(followOutboxesOrProxy, nip65RelayList, privateStorageRelayList, localRelayList, scope) // keeps a cache of the outbox relays for each author val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, proxyRelayList, cache, scope).flow @@ -317,17 +327,7 @@ 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) @@ -346,7 +346,7 @@ class Account( feedFilterListName = settings.defaultHomeFollowList, allFollows = allFollows.flow, locationFlow = geolocationFlow, - followsRelays = followPlusAllMine.flow, + followsRelays = defaultGlobalRelays.flow, blockedRelays = blockedRelayList.flow, proxyRelays = proxyRelayList.flow, caches = feedDecryptionCaches, @@ -361,7 +361,7 @@ class Account( feedFilterListName = settings.defaultStoriesFollowList, allFollows = allFollows.flow, locationFlow = geolocationFlow, - followsRelays = followPlusAllMine.flow, + followsRelays = defaultGlobalRelays.flow, blockedRelays = blockedRelayList.flow, proxyRelays = proxyRelayList.flow, caches = feedDecryptionCaches, @@ -376,7 +376,7 @@ class Account( feedFilterListName = settings.defaultDiscoveryFollowList, allFollows = allFollows.flow, locationFlow = geolocationFlow, - followsRelays = followPlusAllMine.flow, + followsRelays = defaultGlobalRelays.flow, blockedRelays = blockedRelayList.flow, proxyRelays = proxyRelayList.flow, caches = feedDecryptionCaches, @@ -391,7 +391,7 @@ class Account( feedFilterListName = settings.defaultNotificationFollowList, allFollows = allFollows.flow, locationFlow = geolocationFlow, - followsRelays = followPlusAllMine.flow, + followsRelays = defaultGlobalRelays.flow, blockedRelays = blockedRelayList.flow, proxyRelays = proxyRelayList.flow, caches = feedDecryptionCaches, @@ -694,7 +694,8 @@ class Account( fun computeRelayListToBroadcast(event: Event): Set { if (event is MetadataEvent || event is AdvertisedRelayListEvent) { - return followPlusAllMine.flow.value + client.relayStatusFlow().value.available + // everywhere + return followPlusAllMineWithIndex.flow.value + client.relayStatusFlow().value.available } if (event is GiftWrapEvent) { val receiver = event.recipientPubKey() @@ -828,6 +829,8 @@ class Account( } } + fun upgradeAttestations() = otsState.upgradeAttestationsIfNeeded(::sendAutomatic) + suspend fun getFollowSetNotes() = withContext(Dispatchers.Default) { val followSetNotes = LocalCache.getFollowSetNotesFor(userProfile()) @@ -842,8 +845,6 @@ class Account( signer, ) - suspend fun updateAttestations() = sendAutomatic(otsState.updateAttestations()) - suspend fun follow(user: User) = sendMyPublicAndPrivateOutbox(kind3FollowList.follow(user)) suspend fun unfollow(user: User) = sendMyPublicAndPrivateOutbox(kind3FollowList.unfollow(user)) @@ -908,7 +909,7 @@ class Account( } fun sendLiterallyEverywhere(event: Event) { - client.send(event, outboxRelays.flow.value + indexerRelayList.flow.value + client.relayStatusFlow().value.available) + client.send(event, followPlusAllMineWithIndex.flow.value + client.relayStatusFlow().value.available) cache.justConsumeMyOwnEvent(event) } @@ -1779,7 +1780,7 @@ class Account( init { Log.d("AccountRegisterObservers", "Init") - scope.launch(Dispatchers.Default) { + scope.launch { cache.antiSpam.flowSpam.collect { it.cache.spamMessages.snapshot().values.forEach { spammer -> if (!hiddenUsers.isHidden(spammer.pubkeyHex) && spammer.shouldHide()) { @@ -1790,5 +1791,30 @@ class Account( } } } + + scope.launch { + cache.live.newEventBundles.collect { newNotes -> + logTime("Account ${userProfile().toBestDisplayName()} newEventBundle Update with ${newNotes.size} new notes") { + upgradeAttestations() + newNotesPreProcessor.runNew(newNotes) + } + } + } + + scope.launch { + cache.live.deletedEventBundles.collect { newNotes -> + logTime("Account ${userProfile().toBestDisplayName()} deletedEventBundle Update with ${newNotes.size} new notes") { + newNotesPreProcessor.runDeleted(newNotes) + } + } + } + + scope.launch(Dispatchers.IO) { + settings.saveable.debounce(1000).collect { + if (it.accountSettings != null) { + LocalPreferences.saveToEncryptedStorage(it.accountSettings) + } + } + } } } 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 47a626522..3c3268fd7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -20,19 +20,14 @@ */ package com.vitorpamplona.amethyst.model -import android.content.ContentResolver 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.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent -import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent @@ -49,7 +44,6 @@ import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission -import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo @@ -136,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()), @@ -154,21 +147,6 @@ class AccountSettings( fun isWriteable(): Boolean = keyPair.privKey != null || externalSignerPackageName != null - fun createSigner(contentResolver: ContentResolver) = - if (keyPair.privKey != null) { - NostrSignerInternal(keyPair) - } else { - when (val packageName = externalSignerPackageName) { - null -> NostrSignerInternal(keyPair) - else -> - NostrSignerExternal( - pubKey = keyPair.pubKey.toHexKey(), - packageName = packageName, - contentResolver = contentResolver, - ) - } - } - // --- // Zaps and Reactions // --- @@ -268,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/Channel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index 798ffcfad..eb59a304c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -48,7 +48,15 @@ abstract class Channel : NotesGatherer { return new } - open fun participatingAuthors() = notes.mapNotNull { key, value -> value.author } + open fun participatingAuthors(maxTimeLimit: Long) = + notes.mapNotNull { key, value -> + val createdAt = value.createdAt() + if (createdAt != null && createdAt > maxTimeLimit) { + value.author + } else { + null + } + } abstract fun toBestDisplayName(): String diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 05930de5e..6f632d358 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -78,8 +78,8 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUsers import com.vitorpamplona.quartz.nip01Core.verify import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent -import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder import com.vitorpamplona.quartz.nip03Timestamp.VerificationState +import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent import com.vitorpamplona.quartz.nip09Deletions.DeletionIndex @@ -417,7 +417,7 @@ object LocalCache : ILocalCache { private fun isValidHex(key: String): Boolean { if (key.isBlank()) return false if (key.length != 64) return false - if (key.contains(":")) return false + if (key.contains(':')) return false return Hex.isHex(key) } @@ -1842,15 +1842,15 @@ object LocalCache : ILocalCache { note.addRelay(relay) } - var isVerified = + val isVerified = try { val cachePath = Amethyst.instance.nip95cache cachePath.mkdirs() val file = File(cachePath, event.id) if (!file.exists() && (wasVerified || justVerify(event))) { - val stream = FileOutputStream(file) - stream.write(event.decode()) - stream.close() + FileOutputStream(file).use { stream -> + stream.write(event.decode()) + } Log.i( "FileStorageEvent", "NIP95 File received from $relay and saved to disk as $file", @@ -2147,7 +2147,7 @@ object LocalCache : ILocalCache { } } - suspend fun findStatusesForUser(user: User): ImmutableList { + fun findStatusesForUser(user: User): ImmutableList { checkNotInMainThread() return addressables @@ -2164,9 +2164,9 @@ object LocalCache : ILocalCache { .toImmutableList() } - suspend fun findEarliestOtsForNote( + fun findEarliestOtsForNote( note: Note, - resolverBuilder: OtsResolverBuilder, + otsVerifCache: VerificationStateCache, ): Long? { checkNotInMainThread() @@ -2176,7 +2176,7 @@ object LocalCache : ILocalCache { notes.forEach { _, item -> val noteEvent = item.event if ((noteEvent is OtsEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time))) { - (Amethyst.instance.otsVerifCache.cacheVerify(noteEvent, resolverBuilder) as? VerificationState.Verified)?.verifiedTime?.let { stampedTime -> + (otsVerifCache.cacheVerify(noteEvent) as? VerificationState.Verified)?.verifiedTime?.let { stampedTime -> if (minTime == null || stampedTime < (minTime ?: Long.MAX_VALUE)) { minTime = stampedTime } @@ -2191,7 +2191,7 @@ object LocalCache : ILocalCache { fun cachedModificationEventsForNote(note: Note): List? = modificationCache[note.idHex] - suspend fun findLatestModificationForNote(note: Note): List { + fun findLatestModificationForNote(note: Note): List { checkNotInMainThread() val noteAuthor = note.author ?: return emptyList() @@ -2353,9 +2353,9 @@ object LocalCache : ILocalCache { val children = if (noteEvent is WrappedEvent) { noteEvent.host?.id?.let { - getNoteIfExists(it)?.let { - removeFromCache(it) - it.removeAllChildNotes() + getNoteIfExists(it)?.let { it2 -> + removeFromCache(it2) + it2.removeAllChildNotes() } } } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index b813dcfc0..dceb0d3dd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -31,7 +31,6 @@ import com.vitorpamplona.amethyst.ui.note.toShortDisplay import com.vitorpamplona.quartz.experimental.bounties.addedRewardValue import com.vitorpamplona.quartz.experimental.bounties.hasAdditionalReward import com.vitorpamplona.quartz.lightning.LnInvoiceUtil -import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle @@ -101,14 +100,6 @@ class AddressableNote( fun dTag(): String = address.dTag - override fun wasOrShouldBeDeletedBy( - deletionEvents: Set, - deletionAddressables: Set
, - ): Boolean { - val thisEvent = event - return deletionAddressables.contains(address) || (thisEvent != null && deletionEvents.contains(thisEvent.id)) - } - fun toNAddr() = NAddress.create(address.kind, address.pubKeyHex, address.dTag, relayHintUrl()) fun toATag() = ATag(address, relayHintUrl()) @@ -911,14 +902,6 @@ open class Note( } } - open fun wasOrShouldBeDeletedBy( - deletionEvents: Set, - deletionAddressables: Set
, - ): Boolean { - val thisEvent = event - return deletionEvents.contains(idHex) || (thisEvent is AddressableEvent && deletionAddressables.contains(thisEvent.address())) - } - fun toETag(): ETag { val noteEvent = event return if (noteEvent != null) { 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 new file mode 100644 index 000000000..700e5fdc3 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/accountsCache/AccountCacheState.kt @@ -0,0 +1,121 @@ +/** + * 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.accountsCache + +import android.content.ContentResolver +import android.util.Log +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.service.location.LocationState +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler +import com.vitorpamplona.quartz.nip01Core.core.HexKey +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.OtsResolverBuilder +import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +class AccountCacheState( + val geolocationFlow: StateFlow, + val nwcFilterAssembler: NWCPaymentFilterAssembler, + val contentResolverFn: () -> ContentResolver, + val otsResolverBuilder: OtsResolverBuilder, + val cache: LocalCache, + val client: INostrClient, +) { + val accounts = MutableStateFlow>(emptyMap()) + + fun removeAccount(pubkey: HexKey) { + accounts.update { existingAccounts -> + val oldValue = existingAccounts[pubkey] + oldValue?.scope?.cancel() + existingAccounts.minus(pubkey) + } + } + + fun loadAccount(accountSettings: AccountSettings): Account = + loadAccount( + signer = + if (accountSettings.keyPair.privKey != null) { + NostrSignerInternal(accountSettings.keyPair) + } else { + when (val packageName = accountSettings.externalSignerPackageName) { + null -> NostrSignerInternal(accountSettings.keyPair) + else -> + NostrSignerExternal( + pubKey = accountSettings.keyPair.pubKey.toHexKey(), + packageName = packageName, + contentResolver = contentResolverFn(), + ) + } + }, + accountSettings = accountSettings, + ) + + fun loadAccount( + signer: NostrSigner, + accountSettings: AccountSettings, + ): Account { + val cached = accounts.value[signer.pubKey] + if (cached != null) return cached + + return Account( + settings = accountSettings, + signer = signer, + geolocationFlow = geolocationFlow, + nwcFilterAssembler = nwcFilterAssembler, + otsResolverBuilder = otsResolverBuilder, + cache = cache, + client = client, + scope = + CoroutineScope( + Dispatchers.Default + + SupervisorJob() + + CoroutineExceptionHandler { _, throwable -> + Log.e("AccountCacheState", "Account ${signer.pubKey} caught exception: ${throwable.message}", throwable) + }, + ), + ).also { newAccount -> + accounts.update { existingAccounts -> + existingAccounts.plus(Pair(signer.pubKey, newAccount)) + } + } + } + + fun clear() { + accounts.update { existingAccounts -> + existingAccounts.forEach { + it.value.scope.cancel() + } + emptyMap() + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/IncomingOtsEventVerifier.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/IncomingOtsEventVerifier.kt new file mode 100644 index 000000000..d4cc44f70 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/IncomingOtsEventVerifier.kt @@ -0,0 +1,54 @@ +/** + * 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.nip03Timestamp + +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent +import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn + +class IncomingOtsEventVerifier( + private val otsVerifCache: VerificationStateCache, + private val cache: LocalCache, + private val scope: CoroutineScope, +) { + val verifying = + cache.live.newEventBundles + .onEach { newNotes -> + newNotes.forEach(::consume) + }.stateIn( + scope, + SharingStarted.Eagerly, + null, + ) + + fun consume(note: Note) { + note.event?.let { event -> + if (event is OtsEvent) { + otsVerifCache.cacheVerify(event) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt index 763d72419..faae8ee70 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt @@ -27,18 +27,33 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent -import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OkHttpOtsResolverBuilder +import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import java.util.Base64 class OtsState( val signer: NostrSigner, val cache: LocalCache, - val otsResolver: OkHttpOtsResolverBuilder, + val otsResolver: OtsResolverBuilder, val scope: CoroutineScope, val settings: AccountSettings, ) { + var lastTimeItTriedToUpdateAttestations: Long = 0 + + fun upgradeAttestationsIfNeeded(onReady: (List) -> Unit) { + // only tries to upgrade every hour + val now = TimeUtils.now() + if (now - lastTimeItTriedToUpdateAttestations > TimeUtils.ONE_HOUR) { + lastTimeItTriedToUpdateAttestations = now + scope.launch { + onReady(updateAttestations()) + } + } + } + suspend fun updateAttestations(): List { Log.d("Pending Attestations", "Updating ${settings.pendingAttestations.value.size} pending attestations") 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/nip47WalletConnect/NwcSignerState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt index 1cfa440ae..cc59221ed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.model.nip47WalletConnect -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentQueryState import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair @@ -62,6 +62,7 @@ import kotlinx.coroutines.launch */ class NwcSignerState( val signer: NostrSigner, + val nwcFilterAssembler: NWCPaymentFilterAssembler, val cache: LocalCache, val scope: CoroutineScope, val settings: AccountSettings, @@ -163,13 +164,11 @@ class NwcSignerState( relay = walletService.relayUri, ) - Amethyst.instance.sources.nwc - .subscribe(filter) + nwcFilterAssembler.subscribe(filter) scope.launch(Dispatchers.IO) { delay(60000) // waits 1 minute to complete payment. - Amethyst.instance.sources.nwc - .unsubscribe(filter) + nwcFilterAssembler.unsubscribe(filter) } cache.consume(event, zappedNote, true, walletService.relayUri) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt index d1ca69f69..34805ebfd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt @@ -42,7 +42,7 @@ class LiveActivitiesChannel( fun address() = address - override fun relays() = info?.allRelayUrls()?.toSet() ?: super.relays() + override fun relays() = info?.allRelayUrls()?.toSet()?.ifEmpty { null } ?: super.relays() fun relayHintUrl() = relays().firstOrNull() 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..791f72ca5 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt @@ -0,0 +1,181 @@ +/** + * 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() + preferences[TOR_SETTINGS]?.let { + 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 { + 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/serverList/MergedFollowPlusMineRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt index 35474e98a..6498e7bed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.model.serverList import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxOrProxyRelays -import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.CoroutineScope @@ -40,7 +39,6 @@ class MergedFollowPlusMineRelayListsState( val nip65RelayList: Nip65RelayListState, val privateOutboxRelayList: PrivateStorageRelayListState, val localRelayList: LocalRelayListState, - val indexerRelayList: IndexerRelayListState, val scope: CoroutineScope, ) { fun mergeLists(lists: Array>): Set = lists.reduce { acc, set -> acc + set } @@ -53,7 +51,6 @@ class MergedFollowPlusMineRelayListsState( nip65RelayList.inboxFlow, privateOutboxRelayList.flow, localRelayList.flow, - indexerRelayList.flow, ), ::mergeLists, ).onStart { @@ -65,7 +62,6 @@ class MergedFollowPlusMineRelayListsState( nip65RelayList.inboxFlow.value, privateOutboxRelayList.flow.value, localRelayList.flow.value, - indexerRelayList.flow.value, ), ), ) @@ -80,7 +76,6 @@ class MergedFollowPlusMineRelayListsState( nip65RelayList.inboxFlow.value, privateOutboxRelayList.flow.value, localRelayList.flow.value, - indexerRelayList.flow.value, ), ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithIndexAndSearchRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithIndexAndSearchRelayListsState.kt new file mode 100644 index 000000000..68956135b --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithIndexAndSearchRelayListsState.kt @@ -0,0 +1,92 @@ +/** + * 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.serverList + +import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState +import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState +import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxOrProxyRelays +import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListState +import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +class MergedFollowPlusMineWithIndexAndSearchRelayListsState( + val followsOutboxOrProxyRelayList: FollowListOutboxOrProxyRelays, + val nip65RelayList: Nip65RelayListState, + val privateOutboxRelayList: PrivateStorageRelayListState, + val localRelayList: LocalRelayListState, + val indexerRelayList: IndexerRelayListState, + val searchRelayListsState: SearchRelayListState, + val scope: CoroutineScope, +) { + fun mergeLists(lists: Array>): Set = lists.reduce { acc, set -> acc + set } + + val flow: StateFlow> = + combine( + listOf( + followsOutboxOrProxyRelayList.flow, + nip65RelayList.outboxFlow, + nip65RelayList.inboxFlow, + privateOutboxRelayList.flow, + localRelayList.flow, + indexerRelayList.flow, + searchRelayListsState.flow, + ), + ::mergeLists, + ).onStart { + emit( + mergeLists( + arrayOf( + followsOutboxOrProxyRelayList.flow.value, + nip65RelayList.outboxFlow.value, + nip65RelayList.inboxFlow.value, + privateOutboxRelayList.flow.value, + localRelayList.flow.value, + indexerRelayList.flow.value, + searchRelayListsState.flow.value, + ), + ), + ) + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + mergeLists( + arrayOf( + followsOutboxOrProxyRelayList.flow.value, + nip65RelayList.outboxFlow.value, + nip65RelayList.inboxFlow.value, + privateOutboxRelayList.flow.value, + localRelayList.flow.value, + indexerRelayList.flow.value, + searchRelayListsState.flow.value, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithIndexRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithIndexRelayListsState.kt new file mode 100644 index 000000000..ae22655e8 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithIndexRelayListsState.kt @@ -0,0 +1,87 @@ +/** + * 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.serverList + +import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState +import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState +import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxOrProxyRelays +import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListState +import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +class MergedFollowPlusMineWithIndexRelayListsState( + val followsOutboxOrProxyRelayList: FollowListOutboxOrProxyRelays, + val nip65RelayList: Nip65RelayListState, + val privateOutboxRelayList: PrivateStorageRelayListState, + val localRelayList: LocalRelayListState, + val indexerRelayList: IndexerRelayListState, + val scope: CoroutineScope, +) { + fun mergeLists(lists: Array>): Set = lists.reduce { acc, set -> acc + set } + + val flow: StateFlow> = + combine( + listOf( + followsOutboxOrProxyRelayList.flow, + nip65RelayList.outboxFlow, + nip65RelayList.inboxFlow, + privateOutboxRelayList.flow, + localRelayList.flow, + indexerRelayList.flow, + ), + ::mergeLists, + ).onStart { + emit( + mergeLists( + arrayOf( + followsOutboxOrProxyRelayList.flow.value, + nip65RelayList.outboxFlow.value, + nip65RelayList.inboxFlow.value, + privateOutboxRelayList.flow.value, + localRelayList.flow.value, + indexerRelayList.flow.value, + ), + ), + ) + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + mergeLists( + arrayOf( + followsOutboxOrProxyRelayList.flow.value, + nip65RelayList.outboxFlow.value, + nip65RelayList.inboxFlow.value, + privateOutboxRelayList.flow.value, + localRelayList.flow.value, + indexerRelayList.flow.value, + ), + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithSearchRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithSearchRelayListsState.kt new file mode 100644 index 000000000..68e5af6b9 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineWithSearchRelayListsState.kt @@ -0,0 +1,87 @@ +/** + * 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.serverList + +import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState +import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState +import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxOrProxyRelays +import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListState +import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +class MergedFollowPlusMineWithSearchRelayListsState( + val followsOutboxOrProxyRelayList: FollowListOutboxOrProxyRelays, + val nip65RelayList: Nip65RelayListState, + val privateOutboxRelayList: PrivateStorageRelayListState, + val localRelayList: LocalRelayListState, + val searchRelayListsState: SearchRelayListState, + val scope: CoroutineScope, +) { + fun mergeLists(lists: Array>): Set = lists.reduce { acc, set -> acc + set } + + val flow: StateFlow> = + combine( + listOf( + followsOutboxOrProxyRelayList.flow, + nip65RelayList.outboxFlow, + nip65RelayList.inboxFlow, + privateOutboxRelayList.flow, + localRelayList.flow, + searchRelayListsState.flow, + ), + ::mergeLists, + ).onStart { + emit( + mergeLists( + arrayOf( + followsOutboxOrProxyRelayList.flow.value, + nip65RelayList.outboxFlow.value, + nip65RelayList.inboxFlow.value, + privateOutboxRelayList.flow.value, + localRelayList.flow.value, + searchRelayListsState.flow.value, + ), + ), + ) + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + mergeLists( + arrayOf( + followsOutboxOrProxyRelayList.flow.value, + nip65RelayList.outboxFlow.value, + nip65RelayList.inboxFlow.value, + privateOutboxRelayList.flow.value, + localRelayList.flow.value, + searchRelayListsState.flow.value, + ), + ), + ) +} 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..ae9dcd791 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,19 +79,19 @@ 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 -> emit( TorRelayEvaluation( @@ -101,8 +104,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 +114,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 978a23425..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 @@ -34,12 +34,12 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn class ConnectivityFlow( - val context: Context, + context: Context, ) { @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 c0e39c4c7..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 @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.connectivity -import android.app.Application +import android.content.Context import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -34,14 +34,14 @@ import kotlinx.coroutines.flow.stateIn * Tor will connect as soon as status is listened to. */ class ConnectivityManager( - app: Application, + context: Context, scope: CoroutineScope, ) { val status: StateFlow = - ConnectivityFlow(app).status.distinctUntilChanged().stateIn( + ConnectivityFlow(context).status.distinctUntilChanged().stateIn( scope, SharingStarted.WhileSubscribed(30000), - ConnectivityStatus.Off, + ConnectivityStatus.StartingService, ) val isMobileOrNull: StateFlow = @@ -53,4 +53,14 @@ class ConnectivityManager( SharingStarted.WhileSubscribed(2000), (status.value as? ConnectivityStatus.Active)?.isMobile, ) + + val isMobileOrFalse: StateFlow = + status + .map { + (status.value as? ConnectivityStatus.Active)?.isMobile ?: false + }.stateIn( + scope, + SharingStarted.WhileSubscribed(2000), + (status.value as? ConnectivityStatus.Active)?.isMobile ?: false, + ) } 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/crashreports/CrashReportCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/crashreports/CrashReportCache.kt index cef894441..64d4c2795 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/crashreports/CrashReportCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/crashreports/CrashReportCache.kt @@ -41,7 +41,7 @@ class CrashReportCache( null } - suspend fun writeReport(report: String) { + fun writeReport(report: String) { val trace = outputStream() trace.write(report.toByteArray()) trace.close() @@ -50,8 +50,10 @@ class CrashReportCache( suspend fun loadAndDelete(): String? = withContext(Dispatchers.IO) { val stack = - inputStreamOrNull()?.let { inStream -> - InputStreamReader(inStream).readText() + inputStreamOrNull()?.use { inStream -> + InputStreamReader(inStream).use { reader -> + reader.readText() + } } deleteReport() stack diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageCacheFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageCacheFactory.kt index 3fc58d02c..378017d1d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageCacheFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageCacheFactory.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.images -import android.app.Application +import android.content.Context import coil3.disk.DiskCache import coil3.memory.MemoryCache import com.vitorpamplona.amethyst.service.safeCacheDir @@ -28,7 +28,7 @@ import okio.Path.Companion.toOkioPath class ImageCacheFactory { companion object { - fun newDisk(app: Application): DiskCache = + fun newDisk(app: Context): DiskCache = DiskCache .Builder() .directory(app.safeCacheDir().resolve("image_cache").toOkioPath()) @@ -36,7 +36,7 @@ class ImageCacheFactory { .maximumMaxSizeBytes(1024 * 1024 * 1024) // 1GB .build() - fun newMemory(app: Application): MemoryCache = + fun newMemory(app: Context): MemoryCache = MemoryCache .Builder() .maxSizePercent(app) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageLoaderSetup.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageLoaderSetup.kt index b04fa71fc..e4fae3728 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageLoaderSetup.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageLoaderSetup.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.images -import android.app.Application +import android.content.Context import android.os.Build import coil3.ImageLoader import coil3.SingletonImageLoader @@ -57,7 +57,7 @@ class ImageLoaderSetup { @OptIn(DelicateCoilApi::class) fun setup( - app: Application, + app: Context, diskCache: DiskCache, memoryCache: MemoryCache, callFactory: (url: String) -> Call.Factory, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index 4b7723b71..3b0727d02 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -24,16 +24,19 @@ import android.app.NotificationManager import android.content.Context import android.util.Log import androidx.core.content.ContextCompat +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.LocalCache.chatroomList import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds @@ -65,10 +68,11 @@ class EventNotificationConsumer( var matchAccount = false LocalPreferences.allSavedAccounts().forEach { if (!matchAccount && (it.hasPrivKey || it.loggedInWithExternalSigner)) { - LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)?.let { acc -> + LocalPreferences.loadAccountConfigFromEncryptedStorage(it.npub)?.let { acc -> Log.d(TAG, "New Notification Testing if for ${it.npub}") try { - consumeIfMatchesAccount(event, acc) + val account = Amethyst.instance.accountsCache.loadAccount(acc) + consumeIfMatchesAccount(event, account) matchAccount = true } catch (e: Exception) { if (e is CancellationException) throw e @@ -81,34 +85,31 @@ class EventNotificationConsumer( private suspend fun consumeIfMatchesAccount( pushWrappedEvent: GiftWrapEvent, - account: AccountSettings, + account: Account, ) { - val signer = account.createSigner(applicationContext.contentResolver) - - val notificationEvent = pushWrappedEvent.unwrapThrowing(signer) - consumeNotificationEvent(notificationEvent, signer, account) + val notificationEvent = pushWrappedEvent.unwrapThrowing(account.signer) + consumeNotificationEvent(notificationEvent, account) } suspend fun consumeNotificationEvent( notificationEvent: Event, - signer: NostrSigner, - account: AccountSettings, + account: Account, ) { val consumed = LocalCache.hasConsumed(notificationEvent) - Log.d(TAG, "New Notification ${notificationEvent.kind} ${notificationEvent.id} Arrived for ${signer.pubKey} consumed= $consumed") + Log.d(TAG, "New Notification ${notificationEvent.kind} ${notificationEvent.id} Arrived for ${account.signer.pubKey} consumed= $consumed") if (!consumed) { Log.d(TAG, "New Notification was verified") if (!notificationManager().areNotificationsEnabled()) return Log.d(TAG, "Notifications are enabled") - unwrapAndConsume(notificationEvent, signer)?.let { innerEvent -> + unwrapAndConsume(notificationEvent, account.signer)?.let { innerEvent -> Log.d(TAG, "Unwrapped consume ${innerEvent.javaClass.simpleName}") when (innerEvent) { - is PrivateDmEvent -> notify(innerEvent, signer, account) - is LnZapEvent -> notify(innerEvent, signer, account) - is ChatMessageEvent -> notify(innerEvent, signer, account) - is ChatMessageEncryptedFileHeaderEvent -> notify(innerEvent, signer, account) + is PrivateDmEvent -> notify(innerEvent, account) + is LnZapEvent -> notify(innerEvent, account) + is ChatMessageEvent -> notify(innerEvent, account) + is ChatMessageEncryptedFileHeaderEvent -> notify(innerEvent, account) } } } @@ -124,11 +125,11 @@ class EventNotificationConsumer( var matchAccount = false LocalPreferences.allSavedAccounts().forEach { if (!matchAccount && (it.hasPrivKey || it.loggedInWithExternalSigner) && it.npub in npubs) { - LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)?.let { acc -> + LocalPreferences.loadAccountConfigFromEncryptedStorage(it.npub)?.let { accountSettings -> Log.d(TAG, "New Notification Testing if for ${it.npub}") try { - val signer = acc.createSigner(applicationContext.contentResolver) - consumeNotificationEvent(event, signer, acc) + val account = Amethyst.instance.accountsCache.loadAccount(accountSettings) + consumeNotificationEvent(event, account) matchAccount = true } catch (e: Exception) { if (e is CancellationException) throw e @@ -184,22 +185,21 @@ class EventNotificationConsumer( private fun notify( event: ChatMessageEncryptedFileHeaderEvent, - signer: NostrSigner, - acc: AccountSettings, + account: Account, ) { Log.d(TAG, "New ChatMessage File to Notify") if ( // old event being re-broadcasted event.createdAt > TimeUtils.fifteenMinutesAgo() && // don't display if it comes from me. - event.pubKey != signer.pubKey + event.pubKey != account.signer.pubKey ) { // from the user Log.d(TAG, "Notifying") - val chatroomList = LocalCache.getOrCreateChatroomList(signer.pubKey) + val chatroomList = LocalCache.getOrCreateChatroomList(account.signer.pubKey) val chatNote = LocalCache.getNoteIfExists(event.id) ?: return - val chatRoom = event.chatroomKey(signer.pubKey) + val chatRoom = event.chatroomKey(account.signer.pubKey) - val followingKeySet = acc.backupContactList?.unverifiedFollowKeySet()?.toSet() ?: return + val followingKeySet = account.followingKeySet() val isKnownRoom = ( @@ -210,7 +210,11 @@ class EventNotificationConsumer( val content = chatNote.event?.content ?: "" val user = chatNote.author?.toBestDisplayName() ?: "" val userPicture = chatNote.author?.profilePicture() - val noteUri = chatNote.toNEvent() + "?account=" + acc.keyPair.pubKey.toNpub() + val noteUri = + chatNote.toNEvent() + "?account=" + + account.signer.pubKey + .hexToByteArray() + .toNpub() // TODO: Show Image on notification notificationManager() @@ -229,22 +233,21 @@ class EventNotificationConsumer( private fun notify( event: ChatMessageEvent, - signer: NostrSigner, - acc: AccountSettings, + account: Account, ) { Log.d(TAG, "New ChatMessage to Notify") if ( // old event being re-broadcasted event.createdAt > TimeUtils.fifteenMinutesAgo() && // don't display if it comes from me. - event.pubKey != signer.pubKey + event.pubKey != account.signer.pubKey ) { // from the user Log.d(TAG, "Notifying") - val chatroomList = LocalCache.getOrCreateChatroomList(signer.pubKey) + val chatroomList = LocalCache.getOrCreateChatroomList(account.signer.pubKey) val chatNote = LocalCache.getNoteIfExists(event.id) ?: return - val chatRoom = event.chatroomKey(signer.pubKey) + val chatRoom = event.chatroomKey(account.signer.pubKey) - val followingKeySet = acc.backupContactList?.unverifiedFollowKeySet()?.toSet() ?: return + val followingKeySet = account.followingKeySet() val isKnownRoom = chatroomList.rooms.get(chatRoom)?.senderIntersects(followingKeySet) == true || chatroomList.hasSentMessagesTo(chatRoom) @@ -252,7 +255,11 @@ class EventNotificationConsumer( val content = chatNote.event?.content ?: "" val user = chatNote.author?.toBestDisplayName() ?: "" val userPicture = chatNote.author?.profilePicture() - val noteUri = chatNote.toNEvent() + "?account=" + acc.keyPair.pubKey.toNpub() + val noteUri = + chatNote.toNEvent() + "?account=" + + account.signer.pubKey + .hexToByteArray() + .toNpub() notificationManager() .sendDMNotification( event.id, @@ -269,29 +276,32 @@ class EventNotificationConsumer( private suspend fun notify( event: PrivateDmEvent, - signer: NostrSigner, - acc: AccountSettings, + account: Account, ) { Log.d(TAG, "New Nip-04 DM to Notify") - val note = LocalCache.getNoteIfExists(event.id) ?: return - val chatroomList = LocalCache.getOrCreateChatroomList(signer.pubKey) - // old event being re-broadcast if (event.createdAt < TimeUtils.fifteenMinutesAgo()) return - if (signer.pubKey == event.verifiedRecipientPubKey()) { - val followingKeySet = acc.backupContactList?.unverifiedFollowKeySet()?.toSet() ?: return + if (account.signer.pubKey == event.verifiedRecipientPubKey()) { + val note = LocalCache.getNoteIfExists(event.id) ?: return + val chatroomList = LocalCache.getOrCreateChatroomList(account.signer.pubKey) - val chatRoom = event.chatroomKey(signer.pubKey) + val followingKeySet = account.followingKeySet() + + val chatRoom = event.chatroomKey(account.signer.pubKey) val isKnownRoom = chatroomList.rooms.get(chatRoom)?.senderIntersects(followingKeySet) == true || chatroomList.hasSentMessagesTo(chatRoom) if (isKnownRoom) { note.author?.let { - decryptContent(note, signer)?.let { content -> + decryptContent(note, account.signer)?.let { content -> val user = note.author?.toBestDisplayName() ?: "" val userPicture = note.author?.profilePicture() - val noteUri = note.toNEvent() + "?account=" + acc.keyPair.pubKey.toNpub() + val noteUri = + note.toNEvent() + "?account=" + + account.signer.pubKey + .hexToByteArray() + .toNpub() notificationManager() .sendDMNotification(event.id, content, user, event.createdAt, userPicture, noteUri, applicationContext) } @@ -332,8 +342,7 @@ class EventNotificationConsumer( private suspend fun notify( event: LnZapEvent, - signer: NostrSigner, - acc: AccountSettings, + account: Account, ) { Log.d(TAG, "New Zap to Notify") Log.d(TAG, "Notify Start ${event.toNostrUri()}") @@ -355,20 +364,20 @@ class EventNotificationConsumer( Log.d(TAG, "Notify Amount Bigger than 10") - if (event.isTaggedUser(signer.pubKey)) { + if (event.isTaggedUser(account.signer.pubKey)) { val amount = showAmount(event.amount) Log.d(TAG, "Notify Amount $amount") (noteZapRequest.event as? LnZapRequestEvent)?.let { event -> - decryptZapContentAuthor(event, signer)?.let { decryptedEvent -> + decryptZapContentAuthor(event, account.signer)?.let { decryptedEvent -> Log.d(TAG, "Notify Decrypted if Private Zap ${event.id}") val author = LocalCache.getOrCreateUser(decryptedEvent.pubKey) val senderInfo = Pair(author, decryptedEvent.content.ifBlank { null }) if (noteZapped.event?.content != null) { - decryptContent(noteZapped, signer)?.let { decrypted -> + decryptContent(noteZapped, account.signer)?.let { decrypted -> Log.d(TAG, "Notify Decrypted if Private Note") val zappedContent = decrypted.split("\n")[0] @@ -393,7 +402,11 @@ class EventNotificationConsumer( ) } val userPicture = senderInfo.first.profilePicture() - val noteUri = "notifications?account=" + acc.keyPair.pubKey.toNpub() + val noteUri = + "notifications?account=" + + account.signer.pubKey + .hexToByteArray() + .toNpub() Log.d(TAG, "Notify ${event.id} $content $title $noteUri") @@ -424,7 +437,11 @@ class EventNotificationConsumer( ) val userPicture = senderInfo.first.profilePicture() - val noteUri = "notifications?account=" + acc.keyPair.pubKey.toNpub() + val noteUri = + "notifications?account=" + + account.signer.pubKey + .hexToByteArray() + .toNpub() Log.d(TAG, "Notify ${event.id} $title $noteUri") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt index 1938019e4..e07f5677e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt @@ -43,7 +43,7 @@ object NotificationUtils { private const val DM_GROUP_KEY = "com.vitorpamplona.amethyst.DM_NOTIFICATION" private const val ZAP_GROUP_KEY = "com.vitorpamplona.amethyst.ZAP_NOTIFICATION" - fun NotificationManager.getOrCreateDMChannel(applicationContext: Context): NotificationChannel { + fun getOrCreateDMChannel(applicationContext: Context): NotificationChannel { if (dmChannel != null) return dmChannel!! dmChannel = @@ -65,7 +65,7 @@ object NotificationUtils { return dmChannel!! } - fun NotificationManager.getOrCreateZapChannel(applicationContext: Context): NotificationChannel { + fun getOrCreateZapChannel(applicationContext: Context): NotificationChannel { if (zapChannel != null) return zapChannel!! zapChannel = @@ -153,7 +153,7 @@ object NotificationUtils { val imageLoader = ImageLoader(applicationContext) val imageResult = imageLoader.executeBlocking(request) - sendNotification( + sendNotificationInner( id = id, messageBody = messageBody, messageTitle = messageTitle, @@ -165,7 +165,7 @@ object NotificationUtils { applicationContext = applicationContext, ) } else { - sendNotification( + sendNotificationInner( id = id, messageBody = messageBody, messageTitle = messageTitle, @@ -179,7 +179,7 @@ object NotificationUtils { } } - private fun NotificationManager.sendNotification( + private fun NotificationManager.sendNotificationInner( id: String, messageBody: String, messageTitle: String, 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/notifications/RegisterAccounts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt index 43c2279b5..15f051305 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt @@ -59,8 +59,8 @@ class RegisterAccounts( } return mapNotNullAsync(remainingTos) { info -> - val signer = info.accountSettings.createSigner(Amethyst.instance.contentResolver) - RelayAuthEvent.create(info.relays, notificationToken, signer) + val account = Amethyst.instance.accountsCache.loadAccount(info.accountSettings) + RelayAuthEvent.create(info.relays, notificationToken, account.signer) } } @@ -80,7 +80,7 @@ class RegisterAccounts( if (account.hasPrivKey || account.loggedInWithExternalSigner) { Log.d(tag, "Register Account ${account.npub}") - val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(account.npub) + val acc = LocalPreferences.loadAccountConfigFromEncryptedStorage(account.npub) if (acc != null && acc.isWriteable()) { val nip65Read = acc.backupNIP65RelayList?.readRelaysNorm() ?: emptyList() val nip17Read = acc.backupDMRelayList?.relays() ?: emptyList() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManager.kt index 6f67bb444..ebbdb2a30 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManager.kt @@ -30,13 +30,19 @@ import okhttp3.OkHttpClient import java.net.InetSocketAddress import java.net.Proxy +interface IHttpClientManager { + fun getHttpClient(useProxy: Boolean): OkHttpClient + + fun getCurrentProxyPort(useProxy: Boolean): Int? +} + class DualHttpClientManager( userAgent: String, proxyPortProvider: StateFlow, isMobileDataProvider: StateFlow, keyCache: EncryptionKeyCache, scope: CoroutineScope, -) { +) : IHttpClientManager { val factory = OkHttpClientFactory(keyCache) val defaultHttpClient: StateFlow = @@ -60,17 +66,31 @@ class DualHttpClientManager( fun getCurrentProxy(): Proxy? = defaultHttpClient.value.proxy - fun getCurrentProxyPort(useProxy: Boolean): Int? = + override fun getCurrentProxyPort(useProxy: Boolean): Int? = if (useProxy) { (getCurrentProxy()?.address() as? InetSocketAddress)?.port } else { null } - fun getHttpClient(useProxy: Boolean): OkHttpClient = + override fun getHttpClient(useProxy: Boolean): OkHttpClient = if (useProxy) { defaultHttpClient.value } else { defaultHttpClientWithoutProxy.value } } + +object EmptyHttpClientManager : IHttpClientManager { + val rootOkHttpClient by lazy { + OkHttpClient + .Builder() + .followRedirects(true) + .followSslRedirects(true) + .build() + } + + override fun getHttpClient(useProxy: Boolean) = rootOkHttpClient + + override fun getCurrentProxyPort(useProxy: Boolean) = null +} 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 0614fef0d..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.proxyPortFor(videoUri), + proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(videoUri), keepPlaying = true, waveformData = waveform, ) { mediaItem -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCacheFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCacheFactory.kt index 86c82701c..c0a571e7e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCacheFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCacheFactory.kt @@ -20,13 +20,13 @@ */ package com.vitorpamplona.amethyst.service.playback.diskCache -import android.app.Application +import android.content.Context import com.vitorpamplona.amethyst.service.safeCacheDir import kotlinx.coroutines.runBlocking class VideoCacheFactory { companion object { - fun new(app: Application): VideoCache { + fun new(app: Context): VideoCache { val newCache = VideoCache() runBlocking { newCache.initFileCache( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/ExoPlayerPool.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/ExoPlayerPool.kt index 85121a580..1feed28b3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/ExoPlayerPool.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/ExoPlayerPool.kt @@ -37,9 +37,9 @@ import java.util.concurrent.ConcurrentLinkedQueue @OptIn(UnstableApi::class) class ExoPlayerPool( val builder: ExoPlayerBuilder, + private val poolSize: Int, ) { private val playerPool = ConcurrentLinkedQueue() - private val poolSize = SimultaneousPlaybackCalculator.max() private val poolStartingSize = 3 // Exists to avoid exceptions stopping the coroutine diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt index b299a3acc..915f3ff37 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt @@ -20,10 +20,13 @@ */ package com.vitorpamplona.amethyst.service.playback.playerPool +import android.app.PendingIntent import android.content.Context +import android.content.Intent import android.util.Log import android.util.LruCache import androidx.annotation.OptIn +import androidx.core.net.toUri import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi @@ -56,9 +59,10 @@ class SessionListener( class MediaSessionPool( val exoPlayerPool: ExoPlayerPool, val okHttpClient: OkHttpClient, + val appContext: Context, val reset: (MediaSession, Boolean) -> Unit, ) { - val globalCallback = MediaSessionCallback(this) + val globalCallback = MediaSessionCallback(this, appContext) var lastCleanup = TimeUtils.now() // protects from LruCache killing playing sessions @@ -185,6 +189,7 @@ class MediaSessionPool( class MediaSessionCallback( val pool: MediaSessionPool, + val appContext: Context, ) : MediaSession.Callback { @OptIn(UnstableApi::class) override fun onAddMediaItems( @@ -196,7 +201,14 @@ class MediaSessionPool( // set up return call when clicking on the Notification bar mediaItems.firstOrNull()?.mediaMetadata?.extras?.getString("callbackUri")?.let { - mediaSession.setSessionActivity(MainActivity.createIntent(it)) + mediaSession.setSessionActivity( + PendingIntent.getActivity( + appContext, + 0, + Intent(Intent.ACTION_VIEW, it.toUri(), appContext, MainActivity::class.java), + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + ), + ) } return Futures.immediateFuture(mediaItems) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/SimultaneousPlaybackCalculator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/SimultaneousPlaybackCalculator.kt index ce2f050fe..c27ee578c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/SimultaneousPlaybackCalculator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/SimultaneousPlaybackCalculator.kt @@ -27,7 +27,6 @@ import androidx.core.content.getSystemService import androidx.media3.common.MimeTypes import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.mediacodec.MediaCodecUtil -import com.vitorpamplona.amethyst.Amethyst class SimultaneousPlaybackCalculator { companion object { @@ -37,7 +36,7 @@ class SimultaneousPlaybackCalculator { } @OptIn(UnstableApi::class) - fun max(): Int { + fun max(appContext: Context): Int { val maxInstances = try { val info = MediaCodecUtil.getDecoderInfo(MimeTypes.VIDEO_H264, false, false) @@ -54,7 +53,7 @@ class SimultaneousPlaybackCalculator { return maxInstances } - return if (isLowMemory(Amethyst.instance)) { + return if (isLowMemory(appContext)) { 5 } else { 10 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt index eb1c693f8..bb9a8474a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt @@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia import com.vitorpamplona.amethyst.service.playback.playerPool.ExoPlayerBuilder import com.vitorpamplona.amethyst.service.playback.playerPool.ExoPlayerPool import com.vitorpamplona.amethyst.service.playback.playerPool.MediaSessionPool +import com.vitorpamplona.amethyst.service.playback.playerPool.SimultaneousPlaybackCalculator import okhttp3.OkHttpClient class PlaybackService : MediaSessionService() { @@ -42,8 +43,13 @@ class PlaybackService : MediaSessionService() { @OptIn(UnstableApi::class) fun newPool(okHttp: OkHttpClient): MediaSessionPool = MediaSessionPool( - ExoPlayerPool(ExoPlayerBuilder(okHttp)), + exoPlayerPool = + ExoPlayerPool( + ExoPlayerBuilder(okHttp), + poolSize = SimultaneousPlaybackCalculator.max(applicationContext), + ), okHttpClient = okHttp, + appContext = applicationContext, reset = { session, keepPlaying -> (session.player as ExoPlayer).apply { repeatMode = if (keepPlaying) Player.REPEAT_MODE_ONE else Player.REPEAT_MODE_OFF diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt index f03417ace..079fcd01d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt @@ -22,13 +22,13 @@ package com.vitorpamplona.amethyst.service.relayClient import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.EventCollector import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayInsertConfirmationCollector import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl class CacheClientConnector( - val client: NostrClient, + val client: INostrClient, val cache: LocalCache, ) { val receiver = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayLogger.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayLogger.kt index 6b3310725..2ff150571 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayLogger.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayLogger.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient import android.util.Log import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient * Listens to NostrClient's onNotify messages from the relay */ class RelayLogger( - val client: NostrClient, + val client: INostrClient, ) { companion object { val TAG = RelayLogger::class.java.simpleName 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 66e8d8da0..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,10 +25,9 @@ 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.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview @@ -45,15 +44,15 @@ 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: NostrClient, + val client: INostrClient, val torManager: TorManager, 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,14 +69,16 @@ 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() } val torStatus = torManager.status.value if (torStatus is TorServiceStatus.Active) { - torStatus.torControlConnection.signal(TorControlCommands.SIGNAL_DORMANT) + torStatus.torControlConnection?.signal(TorControlCommands.SIGNAL_DORMANT) Log.d("ManageRelayServices", "Pausing Tor Activity") } } else if (it.connectivity is ConnectivityStatus.Active && !client.isActive()) { @@ -85,8 +86,8 @@ class RelayProxyClientConnector( val torStatus = torManager.status.value if (torStatus is TorServiceStatus.Active) { - torStatus.torControlConnection.signal(TorControlCommands.SIGNAL_ACTIVE) - torStatus.torControlConnection.signal(TorControlCommands.SIGNAL_NEWNYM) + torStatus.torControlConnection?.signal(TorControlCommands.SIGNAL_ACTIVE) + torStatus.torControlConnection?.signal(TorControlCommands.SIGNAL_NEWNYM) Log.d("ManageRelayServices", "Resuming Tor Activity with new nym") } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/AuthCoordinator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/AuthCoordinator.kt index 1c9598817..4a3d7d926 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/AuthCoordinator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/AuthCoordinator.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.service.relayClient.authCommand.model import android.util.Log import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayAuthenticator import kotlinx.coroutines.CoroutineScope @@ -32,7 +32,7 @@ class ScreenAuthAccount( ) class AuthCoordinator( - client: NostrClient, + client: INostrClient, scope: CoroutineScope, ) { private val authWithAccounts = ListWithUniqueSetCache { it.account } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt index ea0c7db73..cfc55e4d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt @@ -23,14 +23,14 @@ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers import android.util.Log import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.ammolite.relays.BundledUpdate -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.SubscriptionController import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.Dispatchers abstract class BaseEoseManager( - val client: NostrClient, + val client: INostrClient, val allKeys: () -> Set, ) { protected val logTag: String = this.javaClass.simpleName diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt index dfb326b12..70be7984b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers import com.vitorpamplona.amethyst.service.relays.EOSEByKey import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription @@ -37,7 +37,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl * shares all EOSEs among all users. */ abstract class PerUniqueIdEoseManager( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, val invalidateAfterEose: Boolean = false, ) : BaseEoseManager(client, allKeys) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt index 047238bb0..e696f351f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.EOSEAccountKey import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription @@ -41,7 +41,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl * app reuse the EOSE because it assumes the filter is going to be different */ abstract class PerUserAndFollowListEoseManager( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, val invalidateAfterEose: Boolean = false, ) : BaseEoseManager(client, allKeys) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt index d0d4b4e67..4cc4438c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription @@ -39,7 +39,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl * the subscription disappears and comes back later. */ abstract class PerUserEoseManager( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, val invalidateAfterEose: Boolean = false, ) : BaseEoseManager(client, allKeys) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt index 4ed8dbbbc..d633d0d53 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers import com.vitorpamplona.amethyst.service.relays.EOSERelayList import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -40,7 +40,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl * shares all EOSEs among all users. */ abstract class SingleSubEoseManager( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, val invalidateAfterEose: Boolean = false, ) : BaseEoseManager(client, allKeys) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt index 651cc38dd..c07a30266 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay * filters */ abstract class SingleSubNoEoseCacheEoseManager( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, val invalidateAfterEose: Boolean = false, ) : BaseEoseManager(client, allKeys) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyCoordinator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyCoordinator.kt index 135b9cc74..90a033df1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyCoordinator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyCoordinator.kt @@ -20,11 +20,11 @@ */ package com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayNotifier class NotifyCoordinator( - client: NostrClient, + client: INostrClient, ) { val requests = NotifyRequestsCache() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt index 4eeb50ed5..b18735ea0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt @@ -39,12 +39,12 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterA import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource.UserProfileFilterAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadFilterAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoFilterAssembler -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import kotlinx.coroutines.CoroutineScope class RelaySubscriptionsCoordinator( cache: LocalCache, - client: NostrClient, + client: INostrClient, scope: CoroutineScope, ) { // main one: notifications, dms and account settings diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt index 479d3c73e..600fcac82 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01No import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip59GiftWraps.AccountGiftWrapsEoseManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient // This allows multiple screen to be listening to logged-in accounts. class AccountQueryState( @@ -41,7 +41,7 @@ class AccountQueryState( * This assembler loads everything eech account needs. */ class AccountFilterAssembler( - client: NostrClient, + client: INostrClient, ) : ComposeSubscriptionManager() { val group = listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssemblerSubscription.kt index cae7379f6..2745f17e7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssemblerSubscription.kt @@ -26,7 +26,11 @@ import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable -fun AccountFilterAssemblerSubscription(accountViewModel: AccountViewModel) = AccountFilterAssemblerSubscription(accountViewModel, accountViewModel.dataSources().account) +fun AccountFilterAssemblerSubscription(accountViewModel: AccountViewModel) = + AccountFilterAssemblerSubscription( + accountViewModel, + accountViewModel.dataSources().account, + ) @Composable fun AccountFilterAssemblerSubscription( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt index 0bc24ccd5..de51e0bea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import kotlinx.coroutines.Dispatchers @@ -34,7 +34,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch class AccountMetadataEoseManager( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUserEoseManager(client, allKeys) { override fun user(key: AccountQueryState) = key.account.userProfile() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromInboxRelaysManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromInboxRelaysManager.kt index b443d6a89..d4ec17eb6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromInboxRelaysManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromInboxRelaysManager.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import com.vitorpamplona.quartz.utils.TimeUtils @@ -36,7 +36,7 @@ import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch class AccountNotificationsEoseFromInboxRelaysManager( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUserEoseManager(client, allKeys) { override fun user(key: AccountQueryState) = key.account.userProfile() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt index dc21ec483..a114c0e42 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import kotlinx.coroutines.Dispatchers @@ -36,7 +36,7 @@ import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch class AccountNotificationsEoseFromRandomRelaysManager( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUserEoseManager(client, allKeys) { override fun user(key: AccountQueryState) = key.account.userProfile() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt index 79928cd57..38c677f4d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import kotlinx.coroutines.Dispatchers @@ -34,7 +34,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch class AccountGiftWrapsEoseManager( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUserEoseManager(client, allKeys) { override fun user(key: AccountQueryState) = key.account.userProfile() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt index 797d2ccce..97ef8d588 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.mixChatsLive.ChannelMetadataAndLiveActivityWatcherSubAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats.ChannelLoaderSubAssembler -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient // This allows multiple screen to be listening to tags, even the same tag class ChannelFinderQueryState( @@ -32,7 +32,7 @@ class ChannelFinderQueryState( ) class ChannelFinderFilterAssemblyGroup( - client: NostrClient, + client: INostrClient, ) : ComposeSubscriptionManager() { val group = listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt index 8977d67d4..4400ed68a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt @@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -85,10 +86,11 @@ fun observeChannelNoteAuthors( private fun channelToParticipatingUsers( channel: Channel, accountViewModel: AccountViewModel, + maxTimeLimit: Long = TimeUtils.fifteenMinutesAgo(), ): ImmutableList { val users = mutableSetOf() - channel.participatingAuthors().forEach { + channel.participatingAuthors(maxTimeLimit).forEach { users.add(it) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt index 541605b07..56781d1f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.Channel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats.filterChannelMetadataUpdatesById import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip53LiveActivities.filterLiveStreamUpdatesByAddress import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.utils.mapOfSet @@ -39,7 +39,7 @@ import com.vitorpamplona.quartz.utils.mapOfSet * only one EOSE for everybody. */ class ChannelMetadataAndLiveActivityWatcherSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : SingleSubEoseManager(client, allKeys) { override fun updateFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelLoaderSubAssembler.kt index 5931a339c..5478f8843 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelLoaderSubAssembler.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28P import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubNoEoseCacheEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter /** @@ -35,7 +35,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter * This is one filter for everybody. */ class ChannelLoaderSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : SingleSubNoEoseCacheEoseManager(client, allKeys, invalidateAfterEose = true) { override fun updateFilter(keys: List): List? = filterMissingChannelsById(keys) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt index f1a15eea2..2932b0d86 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt @@ -25,11 +25,11 @@ import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class ChannelMetadataWatcherSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUniqueIdEoseManager(client, allKeys) { override fun updateFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt index 739c118b6..0a3a9129d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChanne import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter /** @@ -33,7 +33,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter * since they are replaceable. */ class LiveActivityWatcherSubAssembly( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUniqueIdEoseManager(client, allKeys) { override fun updateFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt index c9a9685cd..d62fe944b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.NoteEventLoaderSubAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers.EventWatcherSubAssembler -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient // This allows multiple screen to be listening to tags, even the same tag class EventFinderQueryState( @@ -34,7 +34,7 @@ class EventFinderQueryState( ) class EventFinderFilterAssembler( - client: NostrClient, + client: INostrClient, ) : ComposeSubscriptionManager() { val group = listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt index cb16b55d5..f228907e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt @@ -71,7 +71,7 @@ fun filterMissingAddressables(keys: List): List - val default = key.account.followPlusAllMine.flow.value + val default = key.account.followPlusAllMineWithSearch.flow.value if (key.note is AddressableNote && key.note.event == null) { potentialRelaysToFindAddress(key.note).ifEmpty { default }.forEach { relayUrl -> add(relayUrl, key.note.address) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt index 7b5e21544..d76b06042 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt @@ -71,7 +71,7 @@ fun filterMissingEvents(keys: List): List - val default = key.account.followPlusAllMine.flow.value + val default = key.account.followPlusAllMineWithSearch.flow.value if (key.note !is AddressableNote && key.note.event == null) { potentialRelaysToFindEvent(key.note).ifEmpty { default }.forEach { relayUrl -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/NoteEventLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/NoteEventLoaderSubAssembler.kt index 715a6f48e..45f0e7b8e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/NoteEventLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/NoteEventLoaderSubAssembler.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubNoEoseCacheEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient class NoteEventLoaderSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : SingleSubNoEoseCacheEoseManager(client, allKeys, invalidateAfterEose = true) { override fun updateFilter(keys: List) = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt index 36cff0b09..e468c2ba7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt @@ -27,12 +27,12 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFind import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.ammolite.relays.filters.MutableTime -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl class EventWatcherSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : SingleSubEoseManager(client, allKeys) { var lastNotesOnFilter = emptyList() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentFilterAssembler.kt index 71ca89d70..f372693db 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentFilterAssembler.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl // This allows multiple screen to be listening to tags, even the same tag @@ -34,7 +34,7 @@ class NWCPaymentQueryState( ) class NWCPaymentFilterAssembler( - client: NostrClient, + client: INostrClient, ) : ComposeSubscriptionManager() { val group = listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentWatcherSubAssembler.kt index 98e15761e..369c6b69f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentWatcherSubAssembler.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubNoEoseCacheEoseManager -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class NWCPaymentWatcherSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : SingleSubNoEoseCacheEoseManager(client, allKeys) { override fun updateFilter(keys: List): List? { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt index 957107df0..8d5f4c7cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders.UserLoaderSubAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers.UserReportsSubAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers.UserWatcherSubAssembler -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient // This allows multiple screen to be listening to tags, even the same tag class UserFinderQueryState( @@ -35,7 +35,7 @@ class UserFinderQueryState( ) class UserFinderFilterAssembler( - client: NostrClient, + client: INostrClient, ) : ComposeSubscriptionManager() { val group = listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserLoaderSubAssembler.kt index 83c818126..357f29434 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserLoaderSubAssembler.kt @@ -24,12 +24,12 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubNoEoseCacheEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl class UserLoaderSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : SingleSubNoEoseCacheEoseManager(client, allKeys, invalidateAfterEose = true) { override fun updateFilter(keys: List): List? { @@ -46,7 +46,7 @@ class UserLoaderSubAssembler( val defaultRelays = mutableSetOf() keys.mapTo(mutableSetOf()) { it.account }.forEach { - defaultRelays.addAll(it.followPlusAllMine.flow.value) + defaultRelays.addAll(it.followPlusAllMineWithIndexAndSearch.flow.value) it.kind3FollowList.flow.value.authors.forEach { val user = LocalCache.getOrCreateUser(it) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt index d90135544..6b0c40e62 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt @@ -26,13 +26,13 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinder import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.ammolite.relays.filters.MutableTime -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.mapOfSet class UserReportsSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : SingleSubEoseManager(client, allKeys) { var lastUsersOnFilter: Set = emptySet() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt index 1c9cfb477..759d76768 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt @@ -26,12 +26,12 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinder import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.ammolite.relays.filters.MutableTime -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl class UserWatcherSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : SingleSubEoseManager(client, allKeys) { var lastUsersOnFilter: Set = emptySet() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt index 754332de8..022662d95 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.MutableComposeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.MutableQueryState import com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies.SearchWatcherSubAssembler -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -40,7 +40,7 @@ class SearchQueryState( } class SearchFilterAssembler( - client: NostrClient, + client: INostrClient, scope: CoroutineScope, val cache: LocalCache, ) : MutableComposeSubscriptionManager(scope) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt index edd074b73..098d8e071 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEo import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.toHexKey -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress @@ -44,7 +44,7 @@ import com.vitorpamplona.quartz.utils.Hex */ class SearchWatcherSubAssembler( val cache: LocalCache, - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUniqueIdEoseManager(client, allKeys) { override fun updateFilter( @@ -55,27 +55,28 @@ class SearchWatcherSubAssembler( if (mySearchString.isBlank()) return null - val defaultRelays = key.account.followPlusAllMine.flow.value + val defaultRelaysWithIndexAndSearch = key.account.followPlusAllMineWithIndexAndSearch.flow.value + val defaultRelaysWithSearch = key.account.followPlusAllMineWithSearch.flow.value val directFilters = runCatching { if (Hex.isHex(mySearchString)) { val hexKey = Hex.decode(mySearchString).toHexKey() - filterByAuthor(hexKey, defaultRelays) + filterByEvent(hexKey, defaultRelays) + filterByAuthor(hexKey, defaultRelaysWithIndexAndSearch) + filterByEvent(hexKey, defaultRelaysWithSearch) } else { val parsed = Nip19Parser.uriToRoute(mySearchString)?.entity if (parsed != null) { cache.consume(parsed) when (parsed) { - is NSec -> filterByAuthor(parsed.toPubKeyHex(), defaultRelays) - is NPub -> filterByAuthor(parsed.hex, defaultRelays) - is NProfile -> filterByAuthor(parsed.hex, defaultRelays) - is NNote -> filterByEvent(parsed.hex, defaultRelays) - is NEvent -> filterByEvent(parsed.hex, defaultRelays) + is NSec -> filterByAuthor(parsed.toPubKeyHex(), defaultRelaysWithIndexAndSearch) + is NPub -> filterByAuthor(parsed.hex, defaultRelaysWithIndexAndSearch) + is NProfile -> filterByAuthor(parsed.hex, defaultRelaysWithIndexAndSearch) + is NNote -> filterByEvent(parsed.hex, defaultRelaysWithSearch) + is NEvent -> filterByEvent(parsed.hex, defaultRelaysWithSearch) is NEmbed -> emptyList() is NRelay -> emptyList() - is NAddress -> filterByAddress(parsed, defaultRelays) + is NAddress -> filterByAddress(parsed, defaultRelaysWithSearch) else -> emptyList() } } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/RelaySpeedLogger.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/RelaySpeedLogger.kt index 36b0b6c15..19b5821cc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/RelaySpeedLogger.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/RelaySpeedLogger.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.speedLogger import android.util.Log import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient * Listens to NostrClient's onNotify messages from the relay */ class RelaySpeedLogger( - val client: NostrClient, + val client: INostrClient, ) { companion object { val TAG = RelaySpeedLogger::class.java.simpleName 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/service/uploads/blossom/BlossomUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt index 7979093cb..f65550733 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt @@ -96,19 +96,21 @@ class BlossomUploader { checkNotNull(imageInputStream) { "Can't open the image input stream" } - return upload( - imageInputStream, - hash, - payload.size, - fileName, - myContentType, - alt, - sensitiveContent, - serverBaseUrl, - okHttpClient, - httpAuth, - context, - ) + return imageInputStream.use { stream -> + upload( + stream, + hash, + payload.size, + fileName, + myContentType, + alt, + sensitiveContent, + serverBaseUrl, + okHttpClient, + httpAuth, + context, + ) + } } fun encodeAuth(event: BlossomAuthorizationEvent): String { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip95/Nip95CacheFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip95/Nip95CacheFactory.kt index 31910cb74..52d97f0a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip95/Nip95CacheFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip95/Nip95CacheFactory.kt @@ -20,11 +20,11 @@ */ package com.vitorpamplona.amethyst.service.uploads.nip95 -import android.app.Application +import android.content.Context import com.vitorpamplona.amethyst.service.safeCacheDir class Nip95CacheFactory { companion object { - fun new(app: Application) = app.safeCacheDir().resolve("NIP95") + fun new(app: Context) = app.safeCacheDir().resolve("NIP95") } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt index b6acdfde8..ca9814468 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt @@ -109,18 +109,20 @@ class Nip96Uploader { checkNotNull(imageInputStream) { "Can't open the image input stream" } - return upload( - imageInputStream, - length, - myContentType, - alt, - sensitiveContent, - server, - okHttpClient, - onProgress, - httpAuth, - context, - ) + return imageInputStream.use { stream -> + upload( + stream, + length, + myContentType, + alt, + sensitiveContent, + server, + okHttpClient, + onProgress, + httpAuth, + context, + ) + } } suspend fun upload( 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 8e0703715..2b8a9e1f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.amethyst.ui -import android.app.PendingIntent -import android.content.Intent import android.os.Build import android.os.Bundle import android.util.Log @@ -30,7 +28,7 @@ import androidx.activity.enableEdgeToEdge import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.LaunchedEffect -import androidx.core.net.toUri +import androidx.compose.runtime.getValue import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.debugState @@ -43,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 @@ -70,16 +67,14 @@ class MainActivity : AppCompatActivity() { setContent { StringResSetup() - - val sharedPreferencesViewModel = prepareSharedViewModel() - AmethystTheme(sharedPreferencesViewModel) { + AmethystTheme { val accountStateViewModel: AccountStateViewModel = viewModel() LaunchedEffect(key1 = Unit) { accountStateViewModel.loginWithDefaultAccountIfLoggedOff() } - AccountScreen(accountStateViewModel, sharedPreferencesViewModel) + AccountScreen(accountStateViewModel) } } } @@ -130,16 +125,6 @@ class MainActivity : AppCompatActivity() { super.onDestroy() } - - companion object { - fun createIntent(callbackUri: String): PendingIntent = - PendingIntent.getActivity( - Amethyst.instance, - 0, - Intent(Intent.ACTION_VIEW, callbackUri.toUri(), Amethyst.instance, MainActivity::class.java), - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, - ) - } } fun uriToRoute( 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 163c895e2..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 @@ -26,8 +26,10 @@ import android.content.ContextWrapper import android.view.Window import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.compose.ui.window.DialogWindowProvider +import com.vitorpamplona.amethyst.Amethyst // Window utils @Composable @@ -44,7 +46,7 @@ private tailrec fun Context.getActivityWindow(): Window? = } @Composable -fun getActivity(): Activity? = LocalView.current.context.getActivity() +fun getActivity(): Activity? = LocalContext.current.getActivity() tailrec fun Context.getActivity(): ComponentActivity = when (this) { @@ -52,3 +54,8 @@ tailrec fun Context.getActivity(): ComponentActivity = is ContextWrapper -> baseContext.getActivity() else -> throw IllegalStateException("Requires a ComponentActivity to run") } + +@Composable +fun amethystApp(): Amethyst = LocalContext.current.asAmethystApp() + +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/components/toasts/multiline/ErrorList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt index ae1d16be9..7132fc87a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt @@ -75,9 +75,9 @@ fun ErrorListPreview() { runBlocking { withContext(Dispatchers.IO) { - user1 = LocalCache.getOrCreateUser("aaabccaabbccaabbccabdd") - user2 = LocalCache.getOrCreateUser("bbbccabbbccabbbccaabdd") - user3 = LocalCache.getOrCreateUser("ccaadaccaadaccaadaabdd") + user1 = LocalCache.getOrCreateUser("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") + user2 = LocalCache.getOrCreateUser("ca89cb11f1c75d5b6622268ff43d2288ea8b2cb5b9aa996ff9ff704fc904b78b") + user3 = LocalCache.getOrCreateUser("7eb29c126b3628077e2e3d863b917a56b74293aa9d8a9abc26a40ba3f2866baf") } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt index 85e7cc817..720b0786d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt @@ -57,9 +57,9 @@ fun MultiUserErrorMessageContentPreview() { runBlocking { withContext(Dispatchers.IO) { - user1 = LocalCache.getOrCreateUser("aaabccaabbccaabbccabdd") - user2 = LocalCache.getOrCreateUser("bbbccabbbccabbbccaabdd") - user3 = LocalCache.getOrCreateUser("ccaadaccaadaccaadaabdd") + user1 = LocalCache.getOrCreateUser("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") + user2 = LocalCache.getOrCreateUser("ca89cb11f1c75d5b6622268ff43d2288ea8b2cb5b9aa996ff9ff704fc904b78b") + user3 = LocalCache.getOrCreateUser("7eb29c126b3628077e2e3d863b917a56b74293aa9d8a9abc26a40ba3f2866baf") } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt index 8796d527b..b17795168 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.feeds import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter @@ -112,6 +113,8 @@ class FeedContentState( if (lastNoteTime != lastNoteCreatedAtWhenFullyLoaded.value) { lastNoteCreatedAtWhenFullyLoaded.tryEmit(lastNoteTime) } + } else { + lastNoteCreatedAtWhenFullyLoaded.tryEmit(null) } val currentState = _feedContent.value @@ -144,11 +147,15 @@ class FeedContentState( if (deletionEvents.isEmpty()) { oldNotesState.feed.value.list } else { - val deletedEventIds = deletionEvents.flatMapTo(HashSet()) { it.deleteEventIds() } - val deletedEventAddresses = deletionEvents.flatMapTo(HashSet()) { it.deleteAddresses() } oldNotesState.feed.value.list - .filter { !it.wasOrShouldBeDeletedBy(deletedEventIds, deletedEventAddresses) } - .toImmutableList() + .filter { + val noteEvent = it.event + if (noteEvent != null) { + !LocalCache.deletionIndex.hasBeenDeleted(noteEvent) + } else { + false + } + }.toImmutableList() } val newList = @@ -156,6 +163,7 @@ class FeedContentState( .updateListWith(oldList, newItems) .distinctBy { it.idHex } .toImmutableList() + if (!equalImmutableLists(newList, oldNotesState.feed.value.list)) { updateFeed(newList) } 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 a355bb7f9..fd914d1ce 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 @@ -109,7 +109,6 @@ import java.net.URI fun AppNavigation( accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel, - sharedPreferencesViewModel: SharedPreferencesViewModel, listsViewModel: NostrUserListFeedViewModel, ) { val nav = rememberNav() @@ -125,7 +124,7 @@ fun AppNavigation( composable { MessagesScreen(accountViewModel, nav) } composable { VideoScreen(accountViewModel, nav) } composable { DiscoverScreen(accountViewModel, nav) } - composable { NotificationScreen(sharedPreferencesViewModel, accountViewModel, nav) } + composable { NotificationScreen(accountViewModel, nav) } composableFromEnd { ListsScreen(accountViewModel, listsViewModel, nav) } composableArgs { @@ -136,10 +135,10 @@ fun AppNavigation( 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) } @@ -160,6 +159,7 @@ fun AppNavigation( PublicChatChannelScreen( it.id, it.draftId?.let { hex -> accountViewModel.getNoteIfExists(hex) }, + it.replyTo?.let { hex -> accountViewModel.checkGetOrCreateNote(hex) }, accountViewModel, nav, ) @@ -169,6 +169,7 @@ fun AppNavigation( LiveActivityChannelScreen( Address(it.kind, it.pubKeyHex, it.dTag), draft = it.draftId?.let { hex -> accountViewModel.getNoteIfExists(hex) }, + replyTo = it.replyTo?.let { hex -> accountViewModel.checkGetOrCreateNote(hex) }, accountViewModel, nav, ) @@ -179,6 +180,7 @@ fun AppNavigation( EphemeralChatScreen( channelId = RoomId(it.id, relay), draft = it.draftId?.let { hex -> accountViewModel.getNoteIfExists(hex) }, + replyTo = it.replyTo?.let { hex -> accountViewModel.checkGetOrCreateNote(hex) }, accountViewModel = accountViewModel, nav = 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 f6e5a7bba..f3baa96fd 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 @@ -92,7 +92,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 @@ -248,7 +247,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) { @@ -540,7 +539,9 @@ fun ListContent( @Composable private fun RelayStatus(accountViewModel: AccountViewModel) { - val connectedRelaysText by accountViewModel.relayStatusFlow().collectAsStateWithLifecycle() + val connectedRelaysText by accountViewModel.account.client + .relayStatusFlow() + .collectAsStateWithLifecycle() RenderRelayStatus(connectedRelaysText) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/EmptyNav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/EmptyNav.kt index 8e3324ae6..ed2cef42d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/EmptyNav.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/EmptyNav.kt @@ -30,7 +30,7 @@ import kotlin.reflect.KClass @Stable object EmptyNav : INav { - override val scope: CoroutineScope get() = TODO("Not yet implemented") + override val navigationScope: CoroutineScope get() = TODO("Not yet implemented") override val drawerState = DrawerState(DrawerValue.Closed) override fun closeDrawer() = runBlocking { drawerState.close() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/INav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/INav.kt index aefcb060c..17daf99e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/INav.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/INav.kt @@ -28,7 +28,7 @@ import kotlin.reflect.KClass @Stable interface INav { - val scope: CoroutineScope + val navigationScope: CoroutineScope val drawerState: DrawerState fun closeDrawer() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt index fa3b50e99..0336d03e8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt @@ -34,20 +34,20 @@ import kotlin.reflect.KClass @Stable class Nav( val controller: NavHostController, - override val scope: CoroutineScope, + override val navigationScope: CoroutineScope, ) : INav { override val drawerState = DrawerState(DrawerValue.Closed) override fun closeDrawer() { - scope.launch { drawerState.close() } + navigationScope.launch { drawerState.close() } } override fun openDrawer() { - scope.launch { drawerState.open() } + navigationScope.launch { drawerState.open() } } override fun nav(route: Route) { - scope.launch { + navigationScope.launch { if (getRouteWithArguments(controller) != route) { controller.navigate(route) } @@ -55,7 +55,7 @@ class Nav( } override fun nav(computeRoute: suspend () -> Route?) { - scope.launch { + navigationScope.launch { val route = computeRoute() if (route != null && getRouteWithArguments(controller) != route) { controller.navigate(route) @@ -64,7 +64,7 @@ class Nav( } override fun newStack(route: Route) { - scope.launch { + navigationScope.launch { controller.navigate(route) { popUpTo(route) { inclusive = true @@ -75,7 +75,7 @@ class Nav( } override fun popBack() { - scope.launch { + navigationScope.launch { controller.navigateUp() } } @@ -85,7 +85,7 @@ class Nav( route: Route, klass: KClass, ) { - scope.launch { + navigationScope.launch { controller.navigate(route) { popUpTo(klass) { inclusive = true } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/ObservableNav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/ObservableNav.kt index c1ecb0199..03bfae269 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/ObservableNav.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/ObservableNav.kt @@ -28,7 +28,7 @@ import kotlin.reflect.KClass class ObservableNav( val sourceNav: INav, - override val scope: CoroutineScope, + override val navigationScope: CoroutineScope, val onBeforeNavigate: () -> Unit, ) : INav { override val drawerState: DrawerState = sourceNav.drawerState @@ -42,28 +42,28 @@ class ObservableNav( } override fun nav(route: Route) { - scope.launch { + navigationScope.launch { onBeforeNavigate() } sourceNav.nav(route) } override fun nav(computeRoute: suspend () -> Route?) { - scope.launch { + navigationScope.launch { onBeforeNavigate() } sourceNav.nav(computeRoute) } override fun newStack(route: Route) { - scope.launch { + navigationScope.launch { onBeforeNavigate() } sourceNav.newStack(route) } override fun popBack() { - scope.launch { + navigationScope.launch { onBeforeNavigate() } sourceNav.popBack() @@ -73,7 +73,7 @@ class ObservableNav( route: Route, upToClass: KClass, ) { - scope.launch { + navigationScope.launch { onBeforeNavigate() } sourceNav.popUpTo(route, upToClass) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt index 8c5e24612..c69bc5e63 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt @@ -215,6 +215,21 @@ fun routeReplyTo( ): Route? { val noteEvent = note.event return when (noteEvent) { + is ChannelMessageEvent -> { + noteEvent.channelId()?.let { channelId -> + Route.PublicChatChannel(channelId, replyTo = note.idHex) + } + } + is LiveActivitiesChatMessageEvent -> { + noteEvent.activityAddress()?.let { + Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag, replyTo = note.idHex) + } + } + is EphemeralChatEvent -> { + noteEvent.roomId()?.let { + Route.EphemeralChat(it.id, it.relayUrl.url, replyTo = note.idHex) + } + } is PublicMessageEvent -> Route.NewPublicMessage( users = noteEvent.groupKeySet() - account.userProfile().pubkeyHex, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt index 184ca5720..35e2c98f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt @@ -97,6 +97,7 @@ sealed class Route { @Serializable data class PublicChatChannel( val id: String, val draftId: HexKey? = null, + val replyTo: HexKey? = null, ) : Route() @Serializable data class LiveActivityChannel( @@ -104,6 +105,7 @@ sealed class Route { val pubKeyHex: HexKey, val dTag: String, val draftId: HexKey? = null, + val replyTo: HexKey? = null, ) : Route() @Serializable data class RelayInfo( @@ -114,6 +116,7 @@ sealed class Route { val id: String, val relayUrl: String, val draftId: HexKey? = null, + val replyTo: HexKey? = null, ) : Route() @Serializable object NewEphemeralChat : Route() 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/Loaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt index 076e62093..6ada3dda7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt @@ -31,7 +31,9 @@ import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel @@ -44,7 +46,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext import kotlin.coroutines.CoroutineContext @Composable @@ -129,7 +133,14 @@ fun LoadOts( val noteStatus by observeNoteOts(note, accountViewModel) LaunchedEffect(key1 = noteStatus) { - val newOts = accountViewModel.findOtsEventsForNote(noteStatus?.note ?: note) + val newOts = + withContext(Dispatchers.Default) { + LocalCache.findEarliestOtsForNote( + note = noteStatus?.note ?: note, + otsVerifCache = Amethyst.instance.otsVerifCache, + ) + } + earliestDate = if (newOts == null) { GenericLoadable.Empty() 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/NoteQuickActionMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index 16c45590a..a8f43af94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -80,7 +80,6 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.scope import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeEditDraftTo import com.vitorpamplona.amethyst.ui.painterRes 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/LongForm.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt index 235cdea0e..9a4461dac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.note.types +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -28,6 +29,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale @@ -37,11 +39,15 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.note.BaseUserPicture +import com.vitorpamplona.amethyst.ui.note.WatchAuthor import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.amethyst.ui.theme.authorNotePictureForImageHeader import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent @@ -71,16 +77,24 @@ fun LongFormHeader( Column(MaterialTheme.colorScheme.replyModifier) { image?.let { - MyAsyncImage( - imageUrl = it, - contentDescription = stringRes(R.string.preview_card_image_for, it), - contentScale = ContentScale.FillWidth, - mainImageModifier = Modifier.fillMaxWidth(), - loadedImageModifier = Modifier, - accountViewModel = accountViewModel, - onLoadingBackground = { DefaultImageHeaderBackground(note, accountViewModel) }, - onError = { DefaultImageHeader(note, accountViewModel) }, - ) + Box { + MyAsyncImage( + imageUrl = it, + contentDescription = stringRes(R.string.preview_card_image_for, it), + contentScale = ContentScale.FillWidth, + mainImageModifier = Modifier.fillMaxWidth(), + loadedImageModifier = Modifier, + accountViewModel = accountViewModel, + onLoadingBackground = { DefaultImageHeaderBackground(note, accountViewModel) }, + onError = { DefaultImageHeader(note, accountViewModel) }, + ) + + WatchAuthor(baseNote = note, accountViewModel) { user -> + Box(authorNotePictureForImageHeader.align(Alignment.BottomStart)) { + BaseUserPicture(user, Size55dp, accountViewModel, Modifier) + } + } + } } ?: run { DefaultImageHeader(note, accountViewModel, Modifier.fillMaxWidth()) } 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 9588f4a3e..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.proxyPortFor(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 0261ebd11..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.accountSettings, - state.route, - accountStateViewModel, - sharedPreferencesViewModel, + account = state.account, + route = state.route, + accountStateViewModel = accountStateViewModel, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountState.kt index 7976be8ef..08ec6c875 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountState.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner -import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.navigation.routes.Route sealed class AccountState { @@ -36,7 +36,7 @@ sealed class AccountState { @Stable class LoggedIn( - val accountSettings: AccountSettings, + val account: Account, var route: Route? = null, ) : AccountState() { val currentViewModelStore = AccountCentricViewModelStore() 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 04c1c721a..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 @@ -67,11 +65,9 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -84,8 +80,6 @@ class AccountStateViewModel : ViewModel() { private val _accountContent = MutableStateFlow(AccountState.Loading) val accountContent = _accountContent.asStateFlow() - private var collectorJob: Job? = null - fun loginWithDefaultAccountIfLoggedOff() { // pulls account from storage. if (_accountContent.value !is AccountState.LoggedIn) { @@ -98,7 +92,7 @@ class AccountStateViewModel : ViewModel() { private suspend fun loginWithDefaultAccount(route: Route? = null) { val accountSettings = withContext(Dispatchers.IO) { - LocalPreferences.loadCurrentAccountFromEncryptedStorage() + LocalPreferences.loadAccountConfigFromEncryptedStorage() } if (accountSettings != null) { @@ -112,7 +106,6 @@ class AccountStateViewModel : ViewModel() { suspend fun loginAndStartUI( key: String, - torSettings: TorSettings, transientAccount: Boolean, loginWithExternalSigner: Boolean = false, packageName: String = "", @@ -146,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), ) } @@ -184,23 +172,14 @@ class AccountStateViewModel : ViewModel() { accountSettings: AccountSettings, route: Route? = null, ) = withContext(Dispatchers.Main) { - _accountContent.update { AccountState.LoggedIn(accountSettings, route) } - - collectorJob?.cancel() - collectorJob = - viewModelScope.launch(Dispatchers.IO) { - accountSettings.saveable.debounce(1000).collect { - if (it.accountSettings != null) { - LocalPreferences.saveToEncryptedStorage(it.accountSettings) - } - } - } + _accountContent.update { + AccountState.LoggedIn(Amethyst.instance.accountsCache.loadAccount(accountSettings), route) + } } private fun prepareLogoutOrSwitch() = when (val state = _accountContent.value) { is AccountState.LoggedIn -> { - collectorJob?.cancel() state.currentViewModelStore.viewModelStore.clear() } @@ -210,7 +189,6 @@ class AccountStateViewModel : ViewModel() { fun login( key: String, password: String, - torSettings: TorSettings, transientAccount: Boolean, loginWithExternalSigner: Boolean = false, packageName: String = "", @@ -235,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 = "", @@ -279,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) @@ -287,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() @@ -298,7 +271,7 @@ class AccountStateViewModel : ViewModel() { _accountContent.update { AccountState.Loading } - val accountSettings = createNewAccount(torSettings, name) + val accountSettings = createNewAccount(name) LocalPreferences.setDefaultAccount(accountSettings) @@ -319,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) @@ -340,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), ) } @@ -376,7 +345,8 @@ class AccountStateViewModel : ViewModel() { fun currentAccountNPub() = when (val state = _accountContent.value) { is AccountState.LoggedIn -> - state.accountSettings.keyPair.pubKey + state.account.signer.pubKey + .hexToByteArray() .toNpub() else -> null } @@ -387,10 +357,12 @@ class AccountStateViewModel : ViewModel() { // log off and relogin with the 0 account prepareLogoutOrSwitch() LocalPreferences.deleteAccount(accountInfo) + Amethyst.instance.accountsCache.removeAccount(accountInfo.npub.bechToBytes().toHexKey()) loginWithDefaultAccount() } else { // delete without switching logins LocalPreferences.deleteAccount(accountInfo) + Amethyst.instance.accountsCache.removeAccount(accountInfo.npub.bechToBytes().toHexKey()) } } } 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/AccountFeedContentStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt index 2264d833f..79d6d7da0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn -import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState @@ -43,33 +43,35 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedConte import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationSummaryState import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.dal.NotificationFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal.VideoFeedFilter +import kotlinx.coroutines.CoroutineScope class AccountFeedContentStates( - val accountViewModel: AccountViewModel, + val account: Account, + val scope: CoroutineScope, ) { - val homeLive = ChannelFeedContentState(HomeLiveFilter(accountViewModel.account), accountViewModel.viewModelScope) - val homeNewThreads = FeedContentState(HomeNewThreadFeedFilter(accountViewModel.account), accountViewModel.viewModelScope) - val homeReplies = FeedContentState(HomeConversationsFeedFilter(accountViewModel.account), accountViewModel.viewModelScope) + val homeLive = ChannelFeedContentState(HomeLiveFilter(account), scope) + val homeNewThreads = FeedContentState(HomeNewThreadFeedFilter(account), scope) + val homeReplies = FeedContentState(HomeConversationsFeedFilter(account), scope) - val dmKnown = FeedContentState(ChatroomListKnownFeedFilter(accountViewModel.account), accountViewModel.viewModelScope) - val dmNew = FeedContentState(ChatroomListNewFeedFilter(accountViewModel.account), accountViewModel.viewModelScope) + val dmKnown = FeedContentState(ChatroomListKnownFeedFilter(account), scope) + val dmNew = FeedContentState(ChatroomListNewFeedFilter(account), scope) - val videoFeed = FeedContentState(VideoFeedFilter(accountViewModel.account), accountViewModel.viewModelScope) + val videoFeed = FeedContentState(VideoFeedFilter(account), scope) - val discoverFollowSets = FeedContentState(DiscoverFollowSetsFeedFilter(accountViewModel.account), accountViewModel.viewModelScope) - val discoverReads = FeedContentState(DiscoverLongFormFeedFilter(accountViewModel.account), accountViewModel.viewModelScope) - val discoverMarketplace = FeedContentState(DiscoverMarketplaceFeedFilter(accountViewModel.account), accountViewModel.viewModelScope) - val discoverDVMs = FeedContentState(DiscoverNIP89FeedFilter(accountViewModel.account), accountViewModel.viewModelScope) - val discoverLive = FeedContentState(DiscoverLiveFeedFilter(accountViewModel.account), accountViewModel.viewModelScope) - val discoverCommunities = FeedContentState(DiscoverCommunityFeedFilter(accountViewModel.account), accountViewModel.viewModelScope) - val discoverPublicChats = FeedContentState(DiscoverChatFeedFilter(accountViewModel.account), accountViewModel.viewModelScope) + val discoverFollowSets = FeedContentState(DiscoverFollowSetsFeedFilter(account), scope) + val discoverReads = FeedContentState(DiscoverLongFormFeedFilter(account), scope) + val discoverMarketplace = FeedContentState(DiscoverMarketplaceFeedFilter(account), scope) + val discoverDVMs = FeedContentState(DiscoverNIP89FeedFilter(account), scope) + val discoverLive = FeedContentState(DiscoverLiveFeedFilter(account), scope) + val discoverCommunities = FeedContentState(DiscoverCommunityFeedFilter(account), scope) + val discoverPublicChats = FeedContentState(DiscoverChatFeedFilter(account), scope) - val notifications = CardFeedContentState(NotificationFeedFilter(accountViewModel.account), accountViewModel.viewModelScope) - val notificationSummary = NotificationSummaryState(accountViewModel.account) + val notifications = CardFeedContentState(NotificationFeedFilter(account), scope) + val notificationSummary = NotificationSummaryState(account) - val feedListOptions = FollowListState(accountViewModel.account, accountViewModel.viewModelScope) + val feedListOptions = FollowListState(account, scope) - val drafts = FeedContentState(DraftEventsFeedFilter(accountViewModel.account), accountViewModel.viewModelScope) + val drafts = FeedContentState(DraftEventsFeedFilter(account), scope) suspend fun init() { notificationSummary.initializeSuspend() 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 90a93c8b7..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 @@ -28,21 +28,19 @@ import android.util.LruCache import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable +import androidx.compose.runtime.rememberCoroutineScope 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 import com.vitorpamplona.amethyst.AccountInfo -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache import com.vitorpamplona.amethyst.commons.compose.GenericBaseCacheAsync -import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountSettings @@ -50,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 @@ -57,15 +56,19 @@ 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 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.relayClient.reqCommand.RelaySubscriptionsCoordinator +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadingState @@ -80,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 @@ -95,12 +98,14 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata +import com.vitorpamplona.quartz.nip01Core.relay.client.EmptyNostrClient import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal 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.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 @@ -153,33 +158,21 @@ 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( - accountSettings: AccountSettings, - val settings: SharedSettingsState, - val app: Amethyst, + val account: Account, + val settings: UiSettingsState, + val torSettings: TorSettingsFlow, + val dataSources: RelaySubscriptionsCoordinator, + val httpClientBuilder: IRoleBasedHttpClientBuilder, ) : ViewModel(), Dao { - val account = - Account( - settings = accountSettings, - signer = accountSettings.createSigner(app.contentResolver), - geolocationFlow = app.locationManager.geohashStateFlow, - cache = LocalCache, - client = app.client, - scope = viewModelScope, - ) - - val newNotesPreProcessor = EventProcessor(account, LocalCache) - var firstRoute: Route? = null val toastManager = ToastManager() - - val feedStates = AccountFeedContentStates(this) + val feedStates = AccountFeedContentStates(account, viewModelScope) @OptIn(ExperimentalCoroutinesApi::class) val notificationHasNewItems = @@ -655,7 +648,7 @@ class AccountViewModel( message = message, context = context, showErrorIfNoLnAddress = showErrorIfNoLnAddress, - okHttpClient = ::okHttpClientForMoney, + okHttpClient = httpClientBuilder::okHttpClientForMoney, onError = onError, onProgress = onProgress, onPayViaIntent = onPayViaIntent, @@ -713,17 +706,6 @@ class AccountViewModel( fun timestamp(note: Note) = runIOCatching { account.otsState.timestamp(note) } - var lastTimeItTriedToUpdateAttestations: Long = 0 - - fun upgradeAttestations() { - // only tries to upgrade every hour - val now = TimeUtils.now() - if (now - lastTimeItTriedToUpdateAttestations > TimeUtils.ONE_HOUR) { - lastTimeItTriedToUpdateAttestations = now - runIOCatching { account.updateAttestations() } - } - } - fun delete(notes: List) = runIOCatching { account.delete(notes) } fun delete(note: Note) = runIOCatching { account.delete(note) } @@ -902,7 +884,7 @@ class AccountViewModel( onResult: suspend (UrlPreviewState) -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { - UrlCachedPreviewer.previewInfo(url, ::okHttpClientForPreview, onResult) + UrlCachedPreviewer.previewInfo(url, httpClientBuilder::okHttpClientForPreview, onResult) } } @@ -923,9 +905,7 @@ class AccountViewModel( Nip05NostrAddressVerifier() .verifyNip05( nip05, - okHttpClient = { - app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForNIP05(it)) - }, + okHttpClient = httpClientBuilder::okHttpClientForNip05, onSuccess = { // Marks user as verified if (it == pubkeyHex) { @@ -952,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() } } @@ -984,7 +949,7 @@ class AccountViewModel( fun getUserIfExists(hex: HexKey): User? = LocalCache.getUserIfExists(hex) - private fun checkGetOrCreateNote(key: HexKey): Note? = LocalCache.checkGetOrCreateNote(key) + fun checkGetOrCreateNote(key: HexKey): Note? = LocalCache.checkGetOrCreateNote(key) override suspend fun getOrCreateNote(key: HexKey): Note = LocalCache.getOrCreateNote(key) @@ -1026,16 +991,6 @@ class AccountViewModel( fun getAddressableNoteIfExists(key: Address): AddressableNote? = LocalCache.getAddressableNoteIfExists(key) - suspend fun findStatusesForUser(myUser: User) = - withContext(Dispatchers.IO) { - LocalCache.findStatusesForUser(myUser) - } - - suspend fun findOtsEventsForNote(note: Note) = - withContext(Dispatchers.Default) { - LocalCache.findEarliestOtsForNote(note, account.otsResolverBuilder) - } - fun cachedModificationEventsForNote(note: Note) = LocalCache.cachedModificationEventsForNote(note) suspend fun findModificationEventsForNote(note: Note): List = @@ -1099,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( @@ -1134,15 +1089,22 @@ class AccountViewModel( } } - fun setTorSettings(newTorSettings: TorSettings) = runIOCatching { account.settings.setTorSettings(newTorSettings) } - class Factory( - val accountSettings: AccountSettings, - val settings: SharedSettingsState, - val app: Amethyst, + val account: Account, + val settings: UiSettingsState, + val torSettings: TorSettingsFlow, + val dataSources: RelaySubscriptionsCoordinator, + val okHttpClient: RoleBasedHttpClientBuilder, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = AccountViewModel(accountSettings, settings, app) as T + override fun create(modelClass: Class): T = + AccountViewModel( + account, + settings, + torSettings, + dataSources, + okHttpClient, + ) as T } init { @@ -1151,32 +1113,15 @@ class AccountViewModel( feedStates.init() // awaits for init to finish before starting to capture new events. LocalCache.live.newEventBundles.collect { newNotes -> - if (isDebug) { - Log.d( - "Rendering Metrics", - "Update feeds ${this@AccountViewModel} for ${account.userProfile().toBestDisplayName()} with ${newNotes.size} new notes", - ) - } logTime("AccountViewModel newEventBundle Update with ${newNotes.size} new notes") { feedStates.updateFeedsWith(newNotes) - upgradeAttestations() - viewModelScope.launch(Dispatchers.Default) { - newNotesPreProcessor.runNew(newNotes) - } } } } viewModelScope.launch(Dispatchers.Default) { LocalCache.live.deletedEventBundles.collect { newNotes -> - if (isDebug) { - Log.d( - "Rendering Metrics", - "Delete feeds ${this@AccountViewModel} for ${account.userProfile().toBestDisplayName()} with ${newNotes.size} new notes", - ) - } logTime("AccountViewModel deletedEventBundle Update with ${newNotes.size} new notes") { - newNotesPreProcessor.runDeleted(newNotes) feedStates.deleteNotes(newNotes) } } @@ -1314,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( @@ -1431,23 +1376,7 @@ class AccountViewModel( } } - fun proxyPortFor(url: String): Int? = app.okHttpClients.getCurrentProxyPort(account.privacyState.shouldUseTorForVideoDownload(url)) - - fun okHttpClientForNip96(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(url)) - - fun okHttpClientForImage(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForImageDownload(url)) - - fun okHttpClientForVideo(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForVideoDownload(url)) - - fun okHttpClientForMoney(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForMoneyOperations(url)) - - fun okHttpClientForPreview(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForPreviewUrl(url)) - - fun okHttpClientForClean(url: NormalizedRelayUrl): OkHttpClient = app.okHttpClients.getHttpClient(account.torRelayState.shouldUseTorForClean(url)) - - fun okHttpClientForTrustedRelays(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForTrustedRelays()) - - fun dataSources() = app.sources + fun dataSources() = dataSources suspend fun createTempDraftNote(noteEvent: DraftWrapEvent): Note? = draftNoteCache.update(noteEvent) @@ -1562,7 +1491,7 @@ class AccountViewModel( milliSats = milliSats, message = message, nostrRequest = zapRequest, - okHttpClient = ::okHttpClientForMoney, + okHttpClient = httpClientBuilder::okHttpClientForMoney, onProgress = onProgress, context = context, ) @@ -1585,7 +1514,7 @@ class AccountViewModel( viewModelScope.launch { MediaSaverToDisk.saveDownloadingIfNeeded( videoUri = videoUri, - okHttpClient = ::okHttpClientForVideo, + okHttpClient = httpClientBuilder::okHttpClientForVideo, mimeType = mimeType, localContext = localContext, onSuccess = { @@ -1600,8 +1529,6 @@ class AccountViewModel( fun findUsersStartingWithSync(prefix: String) = LocalCache.findUsersStartingWith(prefix, account) - fun relayStatusFlow() = app.client.relayStatusFlow() - fun convertAccounts(loggedInAccounts: List?): Set = loggedInAccounts ?.mapNotNull { @@ -1706,21 +1633,46 @@ 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"), + pubKey = Hex.decode("989c3734c46abac7ce3ce229971581a5a6ee39cdd6aa7261a55823fa7f8c4799"), + forceReplacePubkey = false, + ) + + val client = EmptyNostrClient + + val nwcFilters = NWCPaymentFilterAssembler(client) + + val account = + Account( + settings = AccountSettings(keyPair), + signer = NostrSignerInternal(keyPair), + geolocationFlow = MutableStateFlow(LocationState.LocationResult.Loading), + nwcFilterAssembler = nwcFilters, + otsResolverBuilder = otsResolver, + cache = LocalCache, + client = client, + scope = scope, + ) return AccountViewModel( - AccountSettings( - // blank keys - keyPair = - KeyPair( - privKey = Hex.decode("0f761f8a5a481e26f06605a1d9b3e9eba7a107d351f43c43a57469b788274499"), - pubKey = Hex.decode("989c3734c46abac7ce3ce229971581a5a6ee39cdd6aa7261a55823fa7f8c4799"), - forceReplacePubkey = false, - ), - ), - sharedPreferencesViewModel.sharedPrefs, - Amethyst(), + account = account, + settings = uiState, + torSettings = TorSettingsFlow(torType = MutableStateFlow(TorType.OFF)), + httpClientBuilder = EmptyRoleBasedHttpClientBuilder(), + dataSources = RelaySubscriptionsCoordinator(LocalCache, client, scope), ).also { mockedCache = it } @@ -1733,19 +1685,44 @@ 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"), + ) + + val client = EmptyNostrClient + + val nwcFilters = NWCPaymentFilterAssembler(client) + + val account = + Account( + settings = AccountSettings(keyPair), + signer = NostrSignerInternal(keyPair), + geolocationFlow = MutableStateFlow(LocationState.LocationResult.Loading), + nwcFilterAssembler = nwcFilters, + otsResolverBuilder = otsResolver, + cache = LocalCache, + client = EmptyNostrClient, + scope = scope, + ) return AccountViewModel( - AccountSettings( - // blank keys - keyPair = - KeyPair( - pubKey = Hex.decode("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), - ), - ), - sharedPreferencesViewModel.sharedPrefs, - Amethyst(), + account = account, + 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/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt index 31e72b622..cd12ea159 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn import android.util.Log -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note @@ -29,7 +28,6 @@ import com.vitorpamplona.amethyst.model.privateChats.ChatroomList import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.IEvent -import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent @@ -46,8 +44,6 @@ class EventProcessor( private val cache: LocalCache, ) { private val chatHandler = ChatHandler(account.chatroomList) - private val otsHandler = OtsEventHandler(account) - private val draftHandler = DraftEventHandler(account, cache) private val giftWrapHandler = GiftWrapEventHandler(account, cache, this) @@ -73,7 +69,6 @@ class EventProcessor( ) { when (event) { is ChatroomKeyable -> chatHandler.add(event, eventNote, publicNote) - is OtsEvent -> otsHandler.add(event, eventNote, publicNote) is DraftWrapEvent -> draftHandler.add(event, eventNote, publicNote) is GiftWrapEvent -> giftWrapHandler.add(event, eventNote, publicNote) is SealedRumorEvent -> sealHandler.add(event, eventNote, publicNote) @@ -97,7 +92,6 @@ class EventProcessor( ) { when (event) { is ChatroomKeyable -> chatHandler.delete(event, note) - is OtsEvent -> otsHandler.delete(event, note) is DraftWrapEvent -> draftHandler.delete(event, note) is GiftWrapEvent -> giftWrapHandler.delete(event, note) is SealedRumorEvent -> sealHandler.delete(event, note) @@ -178,18 +172,6 @@ class ChatHandler( } } -class OtsEventHandler( - private val account: Account, -) : EventHandler { - override suspend fun add( - event: OtsEvent, - eventNote: Note, - publicNote: Note, - ) { - Amethyst.instance.otsVerifCache.cacheVerify(event, account.otsResolverBuilder) - } -} - class DraftEventHandler( private val account: Account, private val cache: LocalCache, 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 8168266bd..c431a663a 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 @@ -42,14 +41,13 @@ import com.google.accompanist.permissions.rememberPermissionState import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils import com.vitorpamplona.amethyst.service.relayClient.authCommand.compose.RelayAuthSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountFilterAssemblerSubscription 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 @@ -60,19 +58,20 @@ import kotlinx.coroutines.launch @Composable fun LoggedInPage( - accountSettings: AccountSettings, + account: Account, route: Route?, accountStateViewModel: AccountStateViewModel, - sharedPreferencesViewModel: SharedPreferencesViewModel, ) { val accountViewModel: AccountViewModel = viewModel( key = "AccountViewModel", factory = AccountViewModel.Factory( - accountSettings, - sharedPreferencesViewModel.sharedPrefs, - Amethyst.instance, + account = account, + settings = Amethyst.instance.uiState, + torSettings = Amethyst.instance.torPrefs.value, + dataSources = Amethyst.instance.sources, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder, ), ) @@ -87,12 +86,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) @@ -105,9 +98,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) @@ -117,7 +107,6 @@ fun LoggedInPage( AppNavigation( accountViewModel = accountViewModel, accountStateViewModel = accountStateViewModel, - sharedPreferencesViewModel = sharedPreferencesViewModel, listsViewModel = listsViewModel, ) } @@ -130,27 +119,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) { @@ -165,7 +133,7 @@ fun NotificationRegistration(accountViewModel: AccountViewModel) { scope.launch { PushNotificationUtils.checkAndInit( LocalPreferences.allSavedAccounts(), - accountViewModel::okHttpClientForTrustedRelays, + accountViewModel.httpClientBuilder::okHttpClientForPushRegistration, ) } @@ -180,7 +148,7 @@ fun NotificationRegistration(accountViewModel: AccountViewModel) { scope.launch { PushNotificationUtils.checkAndInit( LocalPreferences.allSavedAccounts(), - accountViewModel::okHttpClientForTrustedRelays, + 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/datasource/ChatroomFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssembler.kt index 740d4cddc..c7a263ce9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssembler.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.datasource import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey // This allows multiple screen to be listening to tags, even the same tag @@ -34,7 +34,7 @@ class ChatroomQueryState( } class ChatroomFilterAssembler( - client: NostrClient, + client: INostrClient, ) : ComposeSubscriptionManager() { val group = listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterSubAssembler.kt index bfa91e4d8..f875f58b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterSubAssembler.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.datasource import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient class ChatroomFilterSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUserAndFollowListEoseManager(client, allKeys) { override fun updateFilter( 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/datasource/ChannelFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt index bc579d454..8d9313021 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies.ChannelFromUserFilterSubAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies.ChannelPublicFilterSubAssembler -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient // This allows multiple screen to be listening to tags, even the same tag class ChannelQueryState( @@ -34,7 +34,7 @@ class ChannelQueryState( ) class ChannelFilterAssembler( - client: NostrClient, + client: INostrClient, ) : ComposeSubscriptionManager() { val group = listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt index df4767a04..700e04709 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt @@ -27,11 +27,11 @@ import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChanne import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelQueryState -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class ChannelFromUserFilterSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUserAndFollowListEoseManager(client, allKeys) { override fun updateFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt index 2330cefae..f5730d428 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt @@ -27,11 +27,11 @@ import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChanne import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelQueryState -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class ChannelPublicFilterSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUniqueIdEoseManager(client, allKeys) { override fun updateFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt index dcce16c5c..2a8adefca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt @@ -47,6 +47,7 @@ import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId fun EphemeralChatChannelView( channelId: RoomId?, draft: Note? = null, + replyTo: Note? = null, accountViewModel: AccountViewModel, nav: INav, ) { @@ -56,6 +57,7 @@ fun EphemeralChatChannelView( PrepareChannelViewModels( baseChannel = ephem, draft = draft, + replyTo = replyTo, accountViewModel = accountViewModel, nav = nav, ) @@ -66,6 +68,7 @@ fun EphemeralChatChannelView( private fun PrepareChannelViewModels( baseChannel: EphemeralChatChannel, draft: Note? = null, + replyTo: Note? = null, accountViewModel: AccountViewModel, nav: INav, ) { @@ -89,6 +92,12 @@ private fun PrepareChannelViewModels( } } + if (replyTo != null) { + LaunchedEffect(replyTo, channelScreenModel, accountViewModel) { + channelScreenModel.reply(replyTo) + } + } + ChannelView( channel = baseChannel, feedViewModel = feedViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt index 49144b988..3811b92db 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt @@ -36,6 +36,7 @@ import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId fun EphemeralChatScreen( channelId: RoomId, draft: Note? = null, + replyTo: Note? = null, accountViewModel: AccountViewModel, nav: INav, ) { @@ -49,7 +50,7 @@ fun EphemeralChatScreen( accountViewModel = accountViewModel, ) { Column(Modifier.padding(it).statusBarsPadding()) { - EphemeralChatChannelView(channelId, draft, accountViewModel, nav) + EphemeralChatChannelView(channelId, draft, replyTo, accountViewModel, nav) } } } 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/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt index aa0371fdc..89f03d6b4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt @@ -47,6 +47,7 @@ import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer fun PublicChatChannelView( channelId: String?, draft: Note? = null, + replyTo: Note? = null, accountViewModel: AccountViewModel, nav: INav, ) { @@ -56,6 +57,7 @@ fun PublicChatChannelView( PrepareChannelViewModels( baseChannel = it, draft = draft, + replyTo = replyTo, accountViewModel = accountViewModel, nav = nav, ) @@ -66,6 +68,7 @@ fun PublicChatChannelView( fun PrepareChannelViewModels( baseChannel: PublicChatChannel, draft: Note? = null, + replyTo: Note? = null, accountViewModel: AccountViewModel, nav: INav, ) { @@ -89,6 +92,12 @@ fun PrepareChannelViewModels( } } + if (replyTo != null) { + LaunchedEffect(replyTo, channelScreenModel, accountViewModel) { + channelScreenModel.reply(replyTo) + } + } + ChannelView( channel = baseChannel, feedViewModel = feedViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt index 1a08e226f..e8c2f8157 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt @@ -37,6 +37,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey fun PublicChatChannelScreen( channelId: HexKey?, draft: Note?, + replyTo: Note? = null, accountViewModel: AccountViewModel, nav: INav, ) { @@ -52,7 +53,7 @@ fun PublicChatChannelScreen( accountViewModel = accountViewModel, ) { Column(Modifier.padding(it).statusBarsPadding()) { - PublicChatChannelView(channelId, draft, accountViewModel, nav) + PublicChatChannelView(channelId, draft, replyTo, accountViewModel, nav) } } } 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/nip53LiveActivities/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt index bb54a6f3f..e147ad2c1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt @@ -47,6 +47,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address fun LiveActivityChannelView( channelId: Address?, draft: Note? = null, + replyTo: Note? = null, accountViewModel: AccountViewModel, nav: INav, ) { @@ -56,6 +57,7 @@ fun LiveActivityChannelView( PrepareChannelViewModels( baseChannel = it, draft = draft, + replyTo = replyTo, accountViewModel = accountViewModel, nav = nav, ) @@ -66,6 +68,7 @@ fun LiveActivityChannelView( fun PrepareChannelViewModels( baseChannel: LiveActivitiesChannel, draft: Note? = null, + replyTo: Note? = null, accountViewModel: AccountViewModel, nav: INav, ) { @@ -89,6 +92,12 @@ fun PrepareChannelViewModels( } } + if (replyTo != null) { + LaunchedEffect(replyTo, channelScreenModel, accountViewModel) { + channelScreenModel.reply(replyTo) + } + } + LiveActivityChannelView( channel = baseChannel, feedViewModel = feedViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt index 4db210440..cef2058a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt @@ -38,6 +38,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address fun LiveActivityChannelScreen( channelId: Address?, draft: Note? = null, + replyTo: Note? = null, accountViewModel: AccountViewModel, nav: INav, ) { @@ -58,7 +59,7 @@ fun LiveActivityChannelScreen( accountViewModel = accountViewModel, ) { Column(Modifier.padding(it)) { - LiveActivityChannelView(channelId, draft, accountViewModel, nav) + LiveActivityChannelView(channelId, draft, replyTo, accountViewModel, nav) } } } 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/chats/rooms/MessagesScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/MessagesScreen.kt index 000ab65f9..f38fcbc57 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/MessagesScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/MessagesScreen.kt @@ -20,26 +20,32 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.singlepane.MessagesSinglePane import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.twopane.MessagesTwoPane +@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @Composable fun MessagesScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val windowSizeClass by accountViewModel.settings.windowSizeClass + val act = LocalContext.current.getActivity() + val windowSizeClass = calculateWindowSizeClass(act) val twoPane by remember { derivedStateOf { - when (windowSizeClass?.widthSizeClass) { + when (windowSizeClass.widthSizeClass) { WindowWidthSizeClass.Compact -> false WindowWidthSizeClass.Expanded, WindowWidthSizeClass.Medium, @@ -49,11 +55,11 @@ fun MessagesScreen( } } - if (twoPane && windowSizeClass != null) { + if (twoPane) { MessagesTwoPane( knownFeedContentState = accountViewModel.feedStates.dmKnown, newFeedContentState = accountViewModel.feedStates.dmNew, - widthSizeClass = windowSizeClass!!.widthSizeClass, + widthSizeClass = windowSizeClass.widthSizeClass, accountViewModel = accountViewModel, nav = nav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssembler.kt index fa72099c3..98fa6d9f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssembler.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient // This allows multiple screen to be listening to tags, even the same tag class ChatroomListState( @@ -30,7 +30,7 @@ class ChatroomListState( ) class ChatroomListFilterAssembler( - client: NostrClient, + client: INostrClient, ) : ComposeSubscriptionManager() { val group = listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt index f8a2a8cff..30a34dc59 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import kotlinx.coroutines.Dispatchers @@ -33,7 +33,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch class DMsFromUserFilterSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUserEoseManager(client, allKeys) { override fun updateFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt index 3c46a3876..196940e2e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import kotlinx.coroutines.Dispatchers @@ -34,7 +34,7 @@ import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch class FollowingEphemeralChatSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUserEoseManager(client, allKeys) { override fun updateFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt index 4f3c10bcc..e08413b26 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import kotlinx.coroutines.Dispatchers @@ -34,7 +34,7 @@ import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch class FollowingPublicChatSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUserEoseManager(client, allKeys) { override fun updateFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt index 47595a0c9..8a09b559d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt @@ -31,9 +31,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import com.google.accompanist.adaptive.FoldAwareConfiguration import com.google.accompanist.adaptive.HorizontalTwoPaneStrategy import com.google.accompanist.adaptive.TwoPane +import com.google.accompanist.adaptive.calculateDisplayFeatures +import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar @@ -68,6 +71,9 @@ fun MessagesTwoPane( } } + val act = LocalContext.current.getActivity() + val displayFeatures = calculateDisplayFeatures(act) + DisappearingScaffold( isInvertedLayout = false, topBar = { @@ -126,7 +132,7 @@ fun MessagesTwoPane( } }, strategy = strategy, - displayFeatures = accountViewModel.settings.displayFeatures.value, + displayFeatures = displayFeatures, foldAwareConfiguration = FoldAwareConfiguration.VerticalFoldsOnly, modifier = Modifier.padding(padding).consumeWindowInsets(padding).fillMaxSize(), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt index 214a74fc5..e0a3c12da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt @@ -30,7 +30,7 @@ import kotlin.reflect.KClass class TwoPaneNav( val nav: INav, - override val scope: CoroutineScope, + override val navigationScope: CoroutineScope, ) : INav { override val drawerState: DrawerState = nav.drawerState @@ -45,7 +45,7 @@ class TwoPaneNav( } override fun nav(computeRoute: suspend () -> Route?) { - scope.launch { + navigationScope.launch { val route = computeRoute() if (route != null) { if (route is Route.Room || route is Route.PublicChatChannel) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFeedFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFeedFilterSubAssembler.kt index 2b891ae8e..2007746cc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFeedFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFeedFilterSubAssembler.kt @@ -23,13 +23,13 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.utils.mapOfSet class CommunityFeedFilterSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : SingleSubEoseManager(client, allKeys) { override fun updateFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt index e69a2dad9..380c8dbfc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient // This allows multiple screen to be listening to tags, even the same tag class CommunityQueryState( @@ -30,7 +30,7 @@ class CommunityQueryState( ) class CommunityFilterAssembler( - client: NostrClient, + client: INostrClient, ) : ComposeSubscriptionManager() { val group = listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt index a8d7a427b..2145fe73d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import kotlinx.coroutines.CoroutineScope // This allows multiple screen to be listening to tags, even the same tag @@ -34,7 +34,7 @@ class DiscoveryQueryState( ) class DiscoveryFilterAssembler( - client: NostrClient, + client: INostrClient, ) : ComposeSubscriptionManager() { val group = listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsSetsAndLiveStreamsSubAssembler2.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsSetsAndLiveStreamsSubAssembler2.kt index dcb7e3e69..a1cb0e32c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsSetsAndLiveStreamsSubAssembler2.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsSetsAndLiveStreamsSubAssembler2.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFol import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.makeFollowSetsFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.makeLiveActivitiesFilter -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import kotlinx.coroutines.Dispatchers @@ -37,7 +37,7 @@ import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch class DiscoveryFollowsSetsAndLiveStreamsSubAssembler2( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUserAndFollowListEoseManager(client, allKeys) { override fun updateFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryLongFormClassifiedsAndDVMSubAssembler1.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryLongFormClassifiedsAndDVMSubAssembler1.kt index 9cb50a1d0..6da653201 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryLongFormClassifiedsAndDVMSubAssembler1.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryLongFormClassifiedsAndDVMSubAssembler1.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.makeLongFormFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.makeContentDVMsFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.makeClassifiedsFilter -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import kotlinx.coroutines.Dispatchers @@ -38,7 +38,7 @@ import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch class DiscoveryLongFormClassifiedsAndDVMSubAssembler1( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUserAndFollowListEoseManager(client, allKeys) { override fun updateFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryPublicChatsAndCommunitiesSubAssembler3.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryPublicChatsAndCommunitiesSubAssembler3.kt index 18feac606..02826f865 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryPublicChatsAndCommunitiesSubAssembler3.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryPublicChatsAndCommunitiesSubAssembler3.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFol import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.makePublicChatsFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.makeCommunitiesFilter -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import kotlinx.coroutines.Dispatchers @@ -37,7 +37,7 @@ import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch class DiscoveryPublicChatsAndCommunitiesSubAssembler3( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUserAndFollowListEoseManager(client, allKeys) { override fun updateFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt index e881f0119..ab004dfc0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt @@ -103,18 +103,26 @@ open class DiscoverChatFeedFilter( } override fun sort(items: Set): List { + // precache to avoid breaking the contract val lastNote = items.associateWith { note -> LocalCache.getPublicChatChannelIfExists(note.idHex)?.lastNote?.createdAt() ?: 0 } - return items - .sortedWith( - compareBy( - { lastNote[it] }, - { it.createdAt() }, - { it.idHex }, - ), - ).reversed() + val createdNote = + items.associateWith { note -> + note.createdAt() ?: 0 + } + + val comparator: Comparator = + compareByDescending { + lastNote[it] + }.thenByDescending { + createdNote[it] + }.thenBy { + it.idHex + } + + return items.sortedWith(comparator) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt index dc67bfc4f..e45a11bed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt @@ -125,13 +125,20 @@ open class DiscoverCommunityFeedFilter( max } - return items - .sortedWith( - compareBy( - { lastNotesCreatedAt[it] }, - { it.createdAt() }, - { it.idHex }, - ), - ).reversed() + val createdNote = + items.associateWith { note -> + note.createdAt() ?: 0 + } + + val comparator: Comparator = + compareByDescending { + lastNotesCreatedAt[it] + }.thenByDescending { + createdNote[it] + }.thenBy { + it.idHex + } + + return items.sortedWith(comparator) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt index eacf82cb3..1731d7248 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt @@ -23,6 +23,14 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.ParticipantListBuilder +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByProxyTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent @@ -33,8 +41,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils open class DiscoverNIP89FeedFilter( val account: Account, ) : AdditiveFeedFilter() { - val lastAnnounced = 365 * 24 * 60 * 60 // 365 Days ago - // TODO better than announced would be last active, as this requires the DVM provider to regularly update the NIP89 announcement + val lastAnnounced = TimeUtils.oneYearAgo() override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followList() @@ -77,7 +84,7 @@ open class DiscoverNIP89FeedFilter( return noteEvent.appMetaData()?.subscription != true && filterParams.match(noteEvent) && noteEvent.includeKind(5300) && - noteEvent.createdAt > TimeUtils.now() - lastAnnounced // && params.match(noteEvent) + noteEvent.createdAt > lastAnnounced // && params.match(noteEvent) } protected open fun innerApplyFilter(collection: Collection): Set = @@ -85,5 +92,39 @@ open class DiscoverNIP89FeedFilter( acceptDVM(it) } - override fun sort(items: Set): List = items.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() + override fun sort(items: Set): List { + val topFilter = account.liveDiscoveryFollowLists.value + val discoveryTopFilterAuthors = + when (topFilter) { + is AuthorsByOutboxTopNavFilter -> topFilter.authors + is MutedAuthorsByOutboxTopNavFilter -> topFilter.authors + is AllFollowsByOutboxTopNavFilter -> topFilter.authors + is SingleCommunityTopNavFilter -> topFilter.authors + is AuthorsByProxyTopNavFilter -> topFilter.authors + is MutedAuthorsByProxyTopNavFilter -> topFilter.authors + is AllFollowsByProxyTopNavFilter -> topFilter.authors + else -> null + } + + val followingKeySet = + discoveryTopFilterAuthors ?: account.kind3FollowList.flow.value.authors + + val counter = ParticipantListBuilder() + val participantCounts = + items.associateWith { counter.countFollowsThatParticipateOn(it, followingKeySet) } + + val createdNote = + items.associateWith { note -> + ((note.event?.createdAt ?: 0) / 86400).toInt() + } + + val feedOrder: Comparator = + compareByDescending { + participantCounts[it] + }.thenByDescending { + createdNote[it] + }.thenBy { it.idHex } + + return items.sortedWith(feedOrder) + } } 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/geohash/datasource/GeoHashFeedFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFeedFilterSubAssembler.kt index 1418f0643..41a522aa2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFeedFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFeedFilterSubAssembler.kt @@ -22,11 +22,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class GeoHashFeedFilterSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUniqueIdEoseManager(client, allKeys) { override fun updateFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssembler.kt index a1d84cf27..e2087c652 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssembler.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl // This allows multiple screen to be listening to tags, even the same tag @@ -38,7 +38,7 @@ class GeohashQueryState( */ @Stable class GeoHashFilterAssembler( - client: NostrClient, + client: INostrClient, ) : ComposeSubscriptionManager() { val group = listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFeedFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFeedFilterSubAssembler.kt index b59790f2c..d5aa32765 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFeedFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFeedFilterSubAssembler.kt @@ -22,11 +22,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class HashtagFeedFilterSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUniqueIdEoseManager(client, allKeys) { override fun updateFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt index ada311d14..5ef49436d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl // This allows multiple screen to be listening to tags, even the same tag @@ -33,7 +33,7 @@ class HashtagQueryState( } class HashtagFilterAssembler( - client: NostrClient, + client: INostrClient, ) : ComposeSubscriptionManager() { val group = listOf( 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/home/dal/HomeLiveFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt index eb3be9e65..686837fb4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt @@ -88,7 +88,9 @@ class HomeLiveFilter( ): Boolean { val liveChannel = channel.info?.let { + val startedAt = it.starts() it.createdAt > timeLimit && + (startedAt == null || (it.createdAt - startedAt) < TimeUtils.ONE_DAY) && it.status() == StatusTag.STATUS.LIVE && filterParams.match(it, channel.relays().toList()) } @@ -156,6 +158,16 @@ class HomeLiveFilter( ): Boolean { val createdAt = note.createdAt() ?: return false val noteEvent = note.event + + if (noteEvent is LiveActivitiesChatMessageEvent) { + val stream = noteEvent.activityAddress() + if (stream == null) return false + val streamChannel = LocalCache.getLiveActivityChannelIfExists(stream) + if (streamChannel == null) return false + + if (streamChannel.info?.status() != StatusTag.STATUS.LIVE) return false + } + return (noteEvent is EphemeralChatEvent || noteEvent is LiveActivitiesChatMessageEvent) && createdAt > timeLimit && filterParams.match(noteEvent, note.relays) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt index e3c7c339e..a6142b5c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows.HomeOutboxEventsEoseManager -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import kotlinx.coroutines.CoroutineScope // This allows multiple screen to be listening to tags, even the same tag @@ -35,7 +35,7 @@ class HomeQueryState( ) class HomeFilterAssembler( - client: NostrClient, + client: INostrClient, ) : ComposeSubscriptionManager() { val group = listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt index f3e8ae122..f6e3d4c47 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt @@ -37,7 +37,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core.f import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core.filterHomePostsByHashtags import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip72Communities.filterHomePostsByAllCommunities import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip72Communities.filterHomePostsByCommunity -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import kotlinx.coroutines.Dispatchers @@ -48,7 +48,7 @@ import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch class HomeOutboxEventsEoseManager( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUserAndFollowListEoseManager(client, allKeys) { override fun updateFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt index 45f673adf..bffecaa20 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt @@ -42,11 +42,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled -import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.feeds.FeedError import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox @@ -58,8 +56,8 @@ import com.vitorpamplona.amethyst.ui.note.MessageSetCompose import com.vitorpamplona.amethyst.ui.note.MultiSetCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.ZapUserSetCompose -import com.vitorpamplona.amethyst.ui.note.elements.ZapTheDevsCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.donations.ShowDonationCard import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.FeedPadding @@ -196,30 +194,6 @@ private fun FeedLoaded( } } -@Composable -private fun ShowDonationCard( - accountViewModel: AccountViewModel, - nav: INav, -) { - if (!accountViewModel.account.hasDonatedInThisVersion()) { - val donated by accountViewModel.account.observeDonatedInThisVersion().collectAsStateWithLifecycle() - if (!donated) { - LoadNote( - BuildConfig.RELEASE_NOTES_ID, - accountViewModel, - ) { loadedNoteId -> - if (loadedNoteId != null) { - ZapTheDevsCard( - loadedNoteId, - accountViewModel, - nav, - ) - } - } - } - } -} - @Composable private fun RenderCardItem( item: Card, 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/donations/ShowDonationCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ShowDonationCard.kt new file mode 100644 index 000000000..814766281 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ShowDonationCard.kt @@ -0,0 +1,53 @@ +/** + * 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.loggedIn.notifications.donations + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.BuildConfig +import com.vitorpamplona.amethyst.ui.components.LoadNote +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel + +@Composable +fun ShowDonationCard( + accountViewModel: AccountViewModel, + nav: INav, +) { + if (!accountViewModel.account.hasDonatedInThisVersion()) { + val donated by accountViewModel.account.observeDonatedInThisVersion().collectAsStateWithLifecycle() + if (!donated) { + LoadNote( + BuildConfig.RELEASE_NOTES_ID, + accountViewModel, + ) { loadedNoteId -> + if (loadedNoteId != null) { + ZapTheDevsCard( + loadedNoteId, + accountViewModel, + nav, + ) + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt similarity index 72% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt index 70c5afa7c..0f97748a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt @@ -18,7 +18,7 @@ * 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.note.elements +package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.donations import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -66,7 +66,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.imageModifier -import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking @@ -75,66 +75,27 @@ import kotlinx.coroutines.runBlocking fun ZapTheDevsCardPreview() { runBlocking(Dispatchers.IO) { val releaseNotes = - """ - { - "id": "0465b20da0adf45dd612024d124e1ed384f7ecd2cd7358e77998828e7bf35fa2", - "pubkey": "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", - "created_at": 1708014675, - "kind": 1, - "tags": [ - [ - "p", - "ca89cb11f1c75d5b6622268ff43d2288ea8b2cb5b9aa996ff9ff704fc904b78b", - "", - "mention" - ], - [ - "p", - "7eb29c126b3628077e2e3d863b917a56b74293aa9d8a9abc26a40ba3f2866baf", - "", - "mention" - ], - [ - "t", - "Amethyst" - ], - [ - "t", - "amethyst" - ], - [ - "zap", - "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", - "wss://vitor.nostr1.com", - "0.6499999761581421" - ], - [ - "zap", - "ca89cb11f1c75d5b6622268ff43d2288ea8b2cb5b9aa996ff9ff704fc904b78b", - "wss://nos.lol", - "0.25" - ], - [ - "zap", - "7eb29c126b3628077e2e3d863b917a56b74293aa9d8a9abc26a40ba3f2866baf", - "wss://vitor.nostr1.com", - "0.10000000149011612" - ], - [ - "r", - "https://github.com/vitorpamplona/amethyst/releases/download/v0.84.2/amethyst-googleplay-universal-v0.84.2.apk" - ], - [ - "r", - "https://github.com/vitorpamplona/amethyst/releases/download/v0.84.2/amethyst-fdroid-universal-v0.84.2.apk" - ] - ], - "content": "#Amethyst v0.84.2: Text alignment fix\n\nBugfixes:\n- Fixes link misalignment in posts\n\nUpdated translations: \n- Czech, German, Swedish, and Portuguese by nostr:npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef\n- French by nostr:npub106efcyntxc5qwl3w8krrhyt626m59ya2nk9f40px5s968u5xdwhsjsr8fz\n\nDownload:\n- [Play Edition](https://github.com/vitorpamplona/amethyst/releases/download/v0.84.2/amethyst-googleplay-universal-v0.84.2.apk )\n- [FOSS Edition - No translations](https://github.com/vitorpamplona/amethyst/releases/download/v0.84.2/amethyst-fdroid-universal-v0.84.2.apk )", - "sig": "e036ecce534e22efd47634c56328af62576ab3a36c565f7c8c5fbea67f48cd46d4041ecfc0ca01dafa0ebe8a0b119d125527a28f88aa30356b80c26dd0953aed" - } - """.trimIndent() + TextNoteEvent( + id = "0465b20da0adf45dd612024d124e1ed384f7ecd2cd7358e77998828e7bf35fa2", + pubKey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", + createdAt = 1708014675, + tags = + arrayOf( + arrayOf("p", "ca89cb11f1c75d5b6622268ff43d2288ea8b2cb5b9aa996ff9ff704fc904b78b", "", "mention"), + arrayOf("p", "7eb29c126b3628077e2e3d863b917a56b74293aa9d8a9abc26a40ba3f2866baf", "", "mention"), + arrayOf("t", "Amethyst"), + arrayOf("t", "amethyst"), + arrayOf("zap", "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", "wss://vitor.nostr1.com", "0.6499999761581421"), + arrayOf("zap", "ca89cb11f1c75d5b6622268ff43d2288ea8b2cb5b9aa996ff9ff704fc904b78b", "wss://nos.lol", "0.25"), + arrayOf("zap", "7eb29c126b3628077e2e3d863b917a56b74293aa9d8a9abc26a40ba3f2866baf", "wss://vitor.nostr1.com", "0.10000000149011612"), + arrayOf("r", "https://github.com/vitorpamplona/amethyst/releases/download/v0.84.2/amethyst-googleplay-universal-v0.84.2.apk"), + arrayOf("r", "https://github.com/vitorpamplona/amethyst/releases/download/v0.84.2/amethyst-fdroid-universal-v0.84.2.apk"), + ), + content = "#Amethyst v0.84.2: Text alignment fix\n\nBugfixes:\n- Fixes link misalignment in posts\n\nUpdated translations: \n- Czech, German, Swedish, and Portuguese by nostr:npub1e2yuky03caw4ke3zy68lg0fz3r4gkt94hx4fjmlelacyljgyk79svn3eef\n- French by nostr:npub106efcyntxc5qwl3w8krrhyt626m59ya2nk9f40px5s968u5xdwhsjsr8fz\n\nDownload:\n- [Play Edition](https://github.com/vitorpamplona/amethyst/releases/download/v0.84.2/amethyst-googleplay-universal-v0.84.2.apk )\n- [FOSS Edition - No translations](https://github.com/vitorpamplona/amethyst/releases/download/v0.84.2/amethyst-fdroid-universal-v0.84.2.apk )", + sig = "e036ecce534e22efd47634c56328af62576ab3a36c565f7c8c5fbea67f48cd46d4041ecfc0ca01dafa0ebe8a0b119d125527a28f88aa30356b80c26dd0953aed", + ) - LocalCache.justConsume(Event.fromJson(releaseNotes), null, false) + LocalCache.consume(releaseNotes, null, true) } val accountViewModel = mockAccountViewModel() @@ -162,7 +123,7 @@ fun ZapTheDevsCard( nav: INav, ) { val releaseNoteState by observeNote(baseNote, accountViewModel) - val releaseNote = releaseNoteState?.note ?: return + val releaseNote = releaseNoteState.note Row(modifier = Modifier.padding(start = Size10dp, end = Size10dp, bottom = Size10dp)) { Card( 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/datasource/UserProfileFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt index f2a5a1d03..c15cad491 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient // This allows multiple screen to be listening to tags, even the same tag class UserProfileQueryState( @@ -30,7 +30,7 @@ class UserProfileQueryState( ) class UserProfileFilterAssembler( - client: NostrClient, + client: INostrClient, ) : ComposeSubscriptionManager() { val group = listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFollowersFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFollowersFilterSubAssembler.kt index dbfd21971..f10984f79 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFollowersFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFollowersFilterSubAssembler.kt @@ -22,11 +22,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class UserProfileFollowersFilterSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUserEoseManager(client, allKeys) { override fun updateFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMediaFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMediaFilterSubAssembler.kt index 40fadd1a4..16bad1339 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMediaFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMediaFilterSubAssembler.kt @@ -22,11 +22,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class UserProfileMediaFilterSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUserEoseManager(client, allKeys) { override fun updateFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMetadataFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMetadataFilterSubAssembler.kt index 7205c28ea..07c905301 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMetadataFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMetadataFilterSubAssembler.kt @@ -23,12 +23,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.utils.mapOfSet class UserProfileMetadataFilterSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : SingleSubEoseManager(client, allKeys) { override fun updateFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfilePostsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfilePostsFilterSubAssembler.kt index a4223f5a6..3d808f64a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfilePostsFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfilePostsFilterSubAssembler.kt @@ -22,11 +22,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class UserProfilePostsFilterSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUserEoseManager(client, allKeys) { override fun updateFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileZapsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileZapsFilterSubAssembler.kt index 04808d724..3483b57fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileZapsFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileZapsFilterSubAssembler.kt @@ -22,11 +22,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class UserProfileZapsFilterSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUserEoseManager(client, allKeys) { override fun updateFilter( 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 6c6898852..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.proxyPortFor(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/threadview/datasources/ThreadFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt index 1ac82c64e..199eaa789 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies.ThreadEventLoaderSubAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies.ThreadFilterSubAssembler import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient // This allows multiple screen to be listening to tags, even the same tag class ThreadQueryState( @@ -34,7 +34,7 @@ class ThreadQueryState( ) class ThreadFilterAssembler( - client: NostrClient, + client: INostrClient, ) : ComposeSubscriptionManager() { val group = listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt index cc3e8a12c..077bb01f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEo import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadQueryState import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter /** @@ -38,7 +38,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter * saves the of the thread EOSEs in the long run to avoid re-downloading */ class ThreadEventLoaderSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUniqueIdEoseManager(client, allKeys, invalidateAfterEose = true) { override fun updateFilter( @@ -46,7 +46,7 @@ class ThreadEventLoaderSubAssembler( since: SincePerRelayMap?, ): List? { val branches = ThreadAssembler().findThreadFor(key.eventId) ?: return null - val defaultRelays = key.account.followPlusAllMine.flow.value + val defaultRelays = key.account.followPlusAllMineWithSearch.flow.value return filterMissingEventsForThread(branches, defaultRelays) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadFilterSubAssembler.kt index f65a9d330..173d53afb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadFilterSubAssembler.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEo import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadQueryState import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter /** @@ -35,7 +35,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter * saves the of the thread EOSEs in the long run to avoid re-downloading */ class ThreadFilterSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUniqueIdEoseManager(client, allKeys) { override fun updateFilter( 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/loggedIn/video/datasource/VideoFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt index 11fba18b7..17a901d46 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.VideoOutboxEventsFilterSubAssembler -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import kotlinx.coroutines.CoroutineScope // This allows multiple screen to be listening to tags, even the same tag @@ -35,7 +35,7 @@ class VideoQueryState( ) class VideoFilterAssembler( - client: NostrClient, + client: INostrClient, ) : ComposeSubscriptionManager() { val group = listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt index ec18f4503..5bc1288e2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt @@ -39,7 +39,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassembl import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip65Follows.filterPictureAndVideoByFollows import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip72Communities.filterPictureAndVideoByAllCommunities import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip72Communities.filterPictureAndVideoByCommunity -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import com.vitorpamplona.quartz.utils.TimeUtils @@ -51,7 +51,7 @@ import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch class VideoOutboxEventsFilterSubAssembler( - client: NostrClient, + client: INostrClient, allKeys: () -> Set, ) : PerUserAndFollowListEoseManager(client, allKeys) { override fun updateFilter( 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/Preview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Preview.kt index 71a5597f9..3b3cb491f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Preview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Preview.kt @@ -27,25 +27,19 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.model.ThemeType -import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel @Composable fun ThemeComparisonColumn(toPreview: @Composable () -> Unit) { Column { Box { - val darkTheme: SharedPreferencesViewModel = viewModel() - darkTheme.updateTheme(ThemeType.DARK) - AmethystTheme(darkTheme) { + AmethystTheme(ThemeType.DARK) { Surface(color = MaterialTheme.colorScheme.background) { toPreview() } } } Box { - val lightTheme: SharedPreferencesViewModel = viewModel() - lightTheme.updateTheme(ThemeType.LIGHT) - AmethystTheme(lightTheme) { + AmethystTheme(ThemeType.LIGHT) { Surface(color = MaterialTheme.colorScheme.background) { toPreview() } } } @@ -56,17 +50,13 @@ fun ThemeComparisonColumn(toPreview: @Composable () -> Unit) { fun ThemeComparisonRow(toPreview: @Composable () -> Unit) { Row { Box(modifier = Modifier.weight(1f)) { - val darkTheme: SharedPreferencesViewModel = viewModel() - darkTheme.updateTheme(ThemeType.DARK) - AmethystTheme(darkTheme) { + AmethystTheme(ThemeType.DARK) { Surface(color = MaterialTheme.colorScheme.background) { toPreview() } } } Box(modifier = Modifier.weight(1f)) { - val lightTheme: SharedPreferencesViewModel = viewModel() - lightTheme.updateTheme(ThemeType.LIGHT) - AmethystTheme(lightTheme) { + AmethystTheme(ThemeType.LIGHT) { Surface(color = MaterialTheme.colorScheme.background) { toPreview() } } } 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 2258053c4..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 @@ -38,6 +38,7 @@ import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color @@ -53,12 +54,13 @@ 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 -import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel private val DarkColorPalette = darkColorScheme( @@ -482,22 +484,30 @@ 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( - sharedPrefsViewModel: SharedPreferencesViewModel, + prefTheme: ThemeType, content: @Composable () -> Unit, ) { val context = LocalContext.current val darkTheme = - when (sharedPrefsViewModel.sharedPrefs.theme) { + when (prefTheme) { ThemeType.DARK -> { - val uiManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager? - uiManager!!.nightMode = UiModeManager.MODE_NIGHT_YES + val uiManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager + uiManager.nightMode = UiModeManager.MODE_NIGHT_YES true } ThemeType.LIGHT -> { - val uiManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager? - uiManager!!.nightMode = UiModeManager.MODE_NIGHT_NO + val uiManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager + uiManager.nightMode = UiModeManager.MODE_NIGHT_NO false } else -> isSystemInDarkTheme() 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/TorServiceStatus.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorServiceStatus.kt index f0bd812a8..3b037e29a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorServiceStatus.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorServiceStatus.kt @@ -26,7 +26,8 @@ sealed class TorServiceStatus { data class Active( val port: Int, ) : TorServiceStatus() { - lateinit var torControlConnection: TorControlConnection + // If internal, it has control. + var torControlConnection: TorControlConnection? = null } object Off : TorServiceStatus() 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 88b8f9d5d..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,12 +22,12 @@ 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, val dmRelaysViaTor: Boolean = true, - val newRelaysViaTor: Boolean = false, + val newRelaysViaTor: Boolean = true, val trustedRelaysViaTor: Boolean = false, val urlPreviewsViaTor: Boolean = false, val profilePicsViaTor: Boolean = false, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsDialog.kt index 782e60b4b..d644ccfbc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsDialog.kt @@ -21,6 +21,10 @@ package com.vitorpamplona.amethyst.ui.tor import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -34,8 +38,8 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType @@ -103,9 +107,14 @@ fun TorDialogContents( ) { val dialogViewModel = viewModel() - LaunchedEffect(dialogViewModel, torSettings) { - dialogViewModel.reset(torSettings) - } + // 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) { + dialogViewModel.reset(torSettings) + torSettings + } TorDialogContents(dialogViewModel, onClose, onPost, onError) } @@ -168,6 +177,8 @@ fun PrivacySettingsBody(dialogViewModel: TorDialogViewModel) { AnimatedVisibility( visible = dialogViewModel.torType.value == TorType.EXTERNAL, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), ) { SettingsRow( R.string.orbot_socks_port, @@ -194,6 +205,8 @@ fun PrivacySettingsBody(dialogViewModel: TorDialogViewModel) { AnimatedVisibility( visible = dialogViewModel.torType.value != TorType.OFF, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), ) { Column( modifier = Modifier.padding(horizontal = 5.dp), 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 e03726f57..d4d765d26 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 @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.tor import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine class TorSettingsFlow( val torType: MutableStateFlow = MutableStateFlow(TorType.INTERNAL), @@ -37,6 +38,42 @@ class TorSettingsFlow( val nip05VerificationsViaTor: MutableStateFlow = MutableStateFlow(false), val nip96UploadsViaTor: MutableStateFlow = MutableStateFlow(false), ) { + // emits at every change in any of the propertyes. + val propertyWatchFlow = + combine( + listOf( + torType, + externalSocksPort, + onionRelaysViaTor, + dmRelaysViaTor, + newRelaysViaTor, + trustedRelaysViaTor, + urlPreviewsViaTor, + profilePicsViaTor, + imagesViaTor, + videosViaTor, + moneyOperationsViaTor, + nip05VerificationsViaTor, + nip96UploadsViaTor, + ), + ) { flows -> + TorSettings( + flows[0] as TorType, + flows[1] as Int, + flows[2] as Boolean, + flows[3] as Boolean, + flows[4] as Boolean, + flows[5] as Boolean, + flows[6] as Boolean, + flows[7] as Boolean, + flows[8] as Boolean, + flows[9] as Boolean, + flows[10] as Boolean, + flows[11] as Boolean, + flows[12] as Boolean, + ) + } + fun toSettings(): TorSettings = TorSettings( torType.value, diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index b8a0233ca..23e828e01 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -164,6 +164,8 @@ Blokovaní uživatelé Nové vlákna Konverzace + Kanál + Fronta moderace Poznámky Odpovědi Vaše @@ -654,6 +656,8 @@ Vysvětlení členům Změna názvu pro nové cíle. Vložit ze schránky + Neplatné NIP-47 URI + URI %1$s není platné přihlašovací URI NIP-47. Pro rozhraní aplikace Tmavé, světlé nebo systémové téma Automaticky načítat obrázky a GIFy @@ -671,6 +675,9 @@ Média přidána do vaší profilové galerie Vytvořeno Pravidla + O nás + Pokyny + Moderátoři Přihlásit se pomocí Amber Aktualizovat svůj stav Chyba při zpracování chybové zprávy @@ -883,10 +890,13 @@ Nahrát v DM Nastavení relé Veřejná domácí relé + Uživatel zveřejňuje svůj obsah na těchto přenašečích Tento typ relé ukládá veškerý váš obsah. Amethyst sem pošle vaše příspěvky a ostatní použijí tato relé, aby našli váš obsah. Vložte mezi 1–3 relé. Mohou to být osobní relé, placená relé nebo veřejná relé. Veřejná schránka relé + Uživatel přijímá oznámení na těchto přenašečích Tento typ relé přijímá všechny odpovědi, komentáře, lajky a zaps k vašim příspěvkům. Vložte mezi 1–3 relé a ujistěte se, že přijímají příspěvky od kohokoli. DM schránka relé + Uživatel přijímá soukromé zprávy na těchto přenašečích Vložte mezi 1–3 relé, která budou sloužit jako vaše soukromá schránka. Ostatní použijí tato relé k posílání DM zpráv vám. DM schránka relé by měla přijímat jakékoli zprávy od kohokoli, ale pouze vám umožnit jejich stahování. Dobré možnosti jsou:\n - inbox.nostr.wine (placené)\n - you.nostr1.com (osobní relé - placené) Soukromá relé Vložte mezi 1–3 relé pro ukládání událostí nikoho jiného, jako jsou koncepty a/nebo nastavení aplikace. V ideálním případě jsou tato relé buď lokální, nebo vyžadují autentizaci před stažením obsahu každého uživatele. @@ -1013,4 +1023,12 @@ Zde zobrazené jazyky nebudou přeloženy. Vyberte jazyk, který chcete odstranit a nechat je znovu přeložit. Pozastavit Hrát + Otevřít rozbalovací nabídku + Možnost %1$s z %2$s + Filtr kanálu, %1$s vybráno + Filtr kanálu, %1$s + Nalezen záznam o pádu + Chcete poslat poslední záznam o pádu do Amethystu v soukromé zprávě? Žádné osobní údaje nebudou sdíleny + Odeslat + Tato zpráva zmizí za %1$d dní diff --git a/amethyst/src/main/res/values-cs/strings.xml b/amethyst/src/main/res/values-cs/strings.xml index 223c59d5f..7d48a433e 100644 --- a/amethyst/src/main/res/values-cs/strings.xml +++ b/amethyst/src/main/res/values-cs/strings.xml @@ -936,4 +936,20 @@ Možnost %1$s z %2$s Filtr kanálu, %1$s vybráno Filtr kanálu, %1$s + Kanál + Fronta moderace + Neplatné NIP-47 URI + URI %1$s není platné přihlašovací URI NIP-47. + Uživatel zveřejňuje svůj obsah na těchto přenašečích + Uživatel přijímá oznámení na těchto přenašečích + Uživatel přijímá soukromé zprávy na těchto přenašečích + Tato zpráva zmizí za %1$d dní + Nahrát video + Nalezen záznam o pádu + Chcete poslat poslední záznam o pádu do Amethystu v soukromé zprávě? Žádné osobní údaje nebudou sdíleny + Odeslat + O nás + Pokyny + Moderátoři + Otevřít rozbalovací nabídku diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index 5d7816ae6..77199f2a6 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -150,6 +150,7 @@ erie gespeichert Video konnte nicht gespeichert werden Bild hochladen Ein Foto aufnehmen + Video aufnehmen Eine Nachricht aufnehmen Eine Nachricht aufnehmen Zum Aufnehmen einer Nachricht gedrückt halten @@ -165,6 +166,8 @@ erie gespeichert Blockierte Benutzer Neue Threads Unterhaltungen + Feed + Moderationswarteschlange Notizen Antworten Deine @@ -658,6 +661,8 @@ anz der Bedingungen ist erforderlich Erklärung an Mitglieder Ändern des Namens für die neuen Ziele. Aus Zwischenablage einfügen + Ungültige NIP-47 URI + Die URI %1$s ist keine gültige NIP-47 Anmelde-URI. Für die App-Benutzeroberfläche Dunkles, helles oder Systemdesign Bilder und GIFs automatisch laden @@ -675,6 +680,9 @@ anz der Bedingungen ist erforderlich Medien wurden zu Ihrer Profilgalerie hinzugefügt Erstellt am Regeln + Über uns + Richtlinien + Moderatoren Mit Amber anmelden Status aktualisieren Fehler beim Verarbeiten der Fehlermeldung @@ -887,10 +895,13 @@ anz der Bedingungen ist erforderlich DM-Upload Relaiseinstellungen Öffentliche Heimrelais + Der Benutzer veröffentlicht seine Inhalte auf diesen Relays Dieser Relais-Typ speichert alle Ihre Inhalte. Amethyst sendet Ihre Beiträge hierher und andere werden diese Relais verwenden, um Ihre Inhalte zu finden. Fügen Sie 1–3 Relais ein. Sie können persönliche Relais, bezahlte Relais oder öffentliche Relais sein. Öffentliche Posteingangsrelais + Der Benutzer erhält Benachrichtigungen auf diesen Relays Dieser Relais-Typ empfängt alle Antworten, Kommentare, Likes und Zaps auf Ihre Beiträge. Fügen Sie 1–3 Relais ein und stellen Sie sicher, dass sie Beiträge von jedem akzeptieren. DM-Posteingangsrelais + Der Benutzer empfängt Direktnachrichten auf diesen Relays Fügen Sie 1–3 Relais ein, die als Ihr privater Posteingang dienen sollen. Andere werden diese Relais verwenden, um Ihnen DMs zu senden. DM-Posteingangsrelais sollten Nachrichten von jedem akzeptieren, aber nur Ihnen erlauben, sie herunterzuladen. Gute Optionen sind:\n - inbox.nostr.wine (bezahlt)\n - you.nostr1.com (persönliche Relais - bezahlt) Private Relais Fügen Sie zwischen 1–3 Relais ein, um Ereignisse zu speichern, die niemand anders sehen kann, wie Ihre Entwürfe und/oder App-Einstellungen. Idealerweise sind diese Relais entweder lokal oder erfordern eine Authentifizierung, bevor Sie die Inhalte eines jeden Benutzers herunterladen. @@ -1017,4 +1028,12 @@ anz der Bedingungen ist erforderlich Die hier angezeigten Sprachen werden nicht übersetzt. Wählen Sie eine Sprache, um sie zu entfernen und lassen Sie sie erneut übersetzen. Pausen Abspielen + Dropdown-Menü öffnen + Option %1$s von %2$s + Feed-Filter, %1$s ausgewählt + Feed-Filter, %1$s + Absturzbericht gefunden + Möchten Sie den letzten Absturzbericht per Direktnachricht an Amethyst senden? Es werden keine persönlichen Daten weitergegeben + Senden + Diese Nachricht verschwindet in %1$d Tagen diff --git a/amethyst/src/main/res/values-de/strings.xml b/amethyst/src/main/res/values-de/strings.xml index a450fb7a2..6476be0e7 100644 --- a/amethyst/src/main/res/values-de/strings.xml +++ b/amethyst/src/main/res/values-de/strings.xml @@ -977,4 +977,19 @@ anz der Bedingungen ist erforderlich Option %1$s von %2$s Feed-Filter, %1$s ausgewählt Feed-Filter, %1$s + Feed + Moderationswarteschlange + Ungültige NIP-47 URI + Die URI %1$s ist keine gültige NIP-47 Anmelde-URI. + Der Benutzer veröffentlicht seine Inhalte auf diesen Relays + Der Benutzer erhält Benachrichtigungen auf diesen Relays + Der Benutzer empfängt Direktnachrichten auf diesen Relays + Diese Nachricht verschwindet in %1$d Tagen + Absturzbericht gefunden + Möchten Sie den letzten Absturzbericht per Direktnachricht an Amethyst senden? Es werden keine persönlichen Daten weitergegeben + Senden + Über uns + Richtlinien + Moderatoren + Dropdown-Menü öffnen diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index 6969b3381..734fbb971 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -148,6 +148,7 @@ Nem sikerült elmenteni a videót Kép feltöltése Kép készítése + Videó rögzítése Hangüzenet rögzítése Hangüzenet rögzítése Hangüzenet rögzítéséhez kattintson és tartsa lenyomva a gombot @@ -891,10 +892,13 @@ Feltöltés közvetlen üzenetbe Átjátszók beállításai Nyilvános kimenő és saját átjátszók + A felhasználó a bejegyzéseit ezeken az átjátszókon teszi közzé Ez az átjátszótípus tárolja az összes tartalmat. Az Amethyst ide küldi az Ön bejegyzéseit, és mások ezeket az átjátszókat fogják használni, hogy megtalálják az Ön tartalmát. Adjon hozzá 1–3 átjátszót. Ezek lehetnek személyes-, fizetett- vagy nyilvános átjátszók. Nyilvános bejövő átjátszók + A felhasználó ezeken az átjátszókon keresztül fogadja az értesítéseket Ez az átjátszótípus fogadja az összes választ, hozzászólást, kedvelést és Zap-et az Ön bejegyzéseire. Ezek lehetnek fizetős vagy ingyenes átjátszók. Az átjátszó üzemeltetője által beállított korlátok korlátozhatják a jó és a rossz értesítések számát. Ha például a hozzászólásokban kéretlen üzenet-támadások érik, a fizetős átjátszók kiszűrhetik a kéretlen tartalmakat. Vegyen fel 1–3 átjátszót. Bejövő közvetlen üzenet-átjátszók + A felhasználó ezeken az átjátszókon keresztül fogadja a közvetlen üzeneteket Adjon hozzá 1–3 átjátszót, hogy privát postafiókként szolgáljon. Mások ezeket az átjátszókat használják, hogy Önnek privát üzeneteket küldjenek. A bejövő privát üzenetek átjátszóinak bárkitől el kell fogadniuk minden üzenetet, de azok letöltését csak Ön engedélyezheti. Jó választási lehetőségek:\n - inbox.nostr.wine (fizetős)\n - auth.nostr1.com (ingyenes)\n - you.nostr1.com (személyes átjátszók - fizetős) Privát saját átjátszók Adjon hozzá 1–3 átjátszót, hogy olyan eseményeket tároljanak, amelyeket senki más nem láthat, például a piszkozatait és/vagy az alkalmazásbeállításait. Ideális esetben ezek az átjátszók vagy helyi szintűek, vagy hitelesítést igényelnek az egyes felhasználói tartalmak letöltése előtt. @@ -1028,4 +1032,5 @@ Összeomlási jelentés megtalálva Szeretné elküldeni a legutóbbi összeomlási jelentést az Amethystnek egy közvetlen üzenetben? A személyes adatait nem osztja meg Küldés + Ez az üzenet %1$d nap múlva eltűnik diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 05590d68f..8425d583c 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -164,6 +164,8 @@ Usuários Novos Tópicos Conversas + Feed + Fila de moderação Notas Respostas Suas @@ -654,6 +656,8 @@ Explicação aos membros Mudando o nome dos novos objetivos. Colar da área de transferência + URI NIP-47 inválido + O URI %1$s não é um URI de login NIP-47 válido. Para a interface do aplicativo Tema Escuro, Claro ou Padrão Carregar automaticamente imagens e GIFs @@ -671,6 +675,9 @@ Mídia adicionada à sua Galeria de Perfil Criado em Regras + Sobre nós + Diretrizes + Moderadores Login com Amber O que você está fazendo? Erro ao analisar mensagem de erro @@ -883,10 +890,13 @@ Envio de DM Configurações de Relay Relés Públicos de Casa + O usuário está publicando seu conteúdo nesses relays Esse tipo de relé armazena todo o seu conteúdo. Amethyst enviará suas postagens aqui e outros usarão esses relés para encontrar seu conteúdo. Insira entre 1–3 relés. Eles podem ser relés pessoais, relés pagos ou relés públicos. Relés Públicos de Caixa de Entrada + O usuário está recebendo notificações nesses relays Esse tipo de relé recebe todas as respostas, comentários, curtidas e zaps para suas postagens. Insira entre 1–3 relés e certifique-se de que aceitem postagens de qualquer pessoa. Relés de Caixa de Entrada de DM + O usuário recebe mensagens diretas (DMs) nesses relays Insira entre 1–3 relés para servir como sua caixa de entrada privada. Outros usarão esses relés para enviar DMs para você. Relés de Caixa de Entrada de DM devem aceitar qualquer mensagem de qualquer pessoa, mas permitir apenas você baixá-las. Boas opções são:\n - inbox.nostr.wine (pago)\n - you.nostr1.com (relés pessoais - pago) Relés privados Insira entre 1–3 retransmissores para armazenar eventos que ninguém mais possa ver, como seus rascunhos e/ou configurações de aplicativo. Idealmente, esses relés são locais ou requerem autenticação antes de baixar o conteúdo de cada usuário. @@ -1013,7 +1023,12 @@ Os idiomas mostrados aqui não serão traduzidos. Selecione um idioma para removê-lo e traduzi-lo novamente. Pausar Reproduzir + Abrir menu suspenso Opção %1$s de %2$s Filtro de feed, %1$s selecionado Filtro de feed, %1$s + Relatório de falha encontrado + Gostaria de enviar o relatório de falha recente para o Amethyst em uma DM? Nenhuma informação pessoal será compartilhada + Enviar + Esta mensagem desaparecerá em %1$d dias diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 7d5512a82..3c7ce5d03 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -164,6 +164,8 @@ Blockerade användare Nya trådar Konversationer + Flöde + Moderationskö Anteckningar Svar Dina @@ -653,6 +655,8 @@ Förklaring till medlemmar Ändra namnet för de nya målen. Klistra in från urklipp + Ogiltig NIP-47 URI + URI %1$s är inte en giltig NIP-47 inloggnings-URI. För appens gränssnitt Mörkt, Ljust eller Systemtema Ladda automatiskt bilder och GIFs @@ -670,6 +674,9 @@ Media har lagts till i ditt profilgalleri Skapad den Regler + Om oss + Riktlinjer + Moderatorer Logga in med Amber Uppdatera din status Fel vid tolkning av felmeddelande @@ -882,10 +889,13 @@ DM uppladdning Relä inställningar Offentliga hemreläer + Användaren publicerar sitt innehåll på dessa reläer Denna typ av relä lagrar allt ditt innehåll. Amethyst skickar dina inlägg hit och andra kommer att använda dessa reläer för att hitta ditt innehåll. Sätt in mellan 1–3 reläer. De kan vara personliga reläer, betalda reläer eller offentliga reläer. Offentliga inkorgsreläer + Användaren tar emot aviseringar på dessa reläer Denna typ av relä tar emot alla svar, kommentarer, gillanden och zaps till dina inlägg. Sätt in mellan 1–3 reläer och se till att de accepterar inlägg från vem som helst. DM inkorgsreläer + Användaren tar emot DM:s på dessa reläer Sätt in mellan 1–3 reläer som ska fungera som din privata inkorg. Andra kommer att använda dessa reläer för att skicka DM till dig. DM inkorgsreläer bör acceptera alla meddelanden från vem som helst, men endast tillåta dig att ladda ner dem. Bra alternativ är:\n - inbox.nostr.wine (betald)\n - you.nostr1.com (personliga reläer - betald) Privata reläer Infoga mellan 1–3 reläer för att lagra händelser som ingen annan kan se, som dina Utkast och/eller appinställningar. Helst är dessa reläer antingen lokala eller kräver autentisering innan du laddar ner varje användares innehåll. @@ -1012,7 +1022,12 @@ Språk som visas här kommer inte att översättas. Välj ett språk för att ta bort det och få det översatt igen. Pausa Spela + Öppna rullgardinsmeny Alternativ %1$s av %2$s Flödesfilter, %1$s valt Flödesfilter, %1$s + Kraschrapport hittad + Vill du skicka den senaste kraschrapporten till Amethyst i ett DM? Ingen personlig information kommer att delas + Skicka + Detta meddelande försvinner om %1$d dagar diff --git a/amethyst/src/play/AndroidManifest.xml b/amethyst/src/play/AndroidManifest.xml index c48a43dec..d0d019e9a 100644 --- a/amethyst/src/play/AndroidManifest.xml +++ b/amethyst/src/play/AndroidManifest.xml @@ -19,6 +19,8 @@ android:name="com.google.android.datatransport.runtime.scheduling.jobscheduling.JobInfoSchedulerService" tools:node="remove"> + + diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt index 57e045df1..58c1a3844 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt @@ -28,8 +28,6 @@ import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences -import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateDMChannel -import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateZapChannel import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import kotlinx.coroutines.CoroutineExceptionHandler @@ -92,8 +90,8 @@ class PushNotificationReceiverService : FirebaseMessagingService() { PushNotificationUtils.checkAndInit(token, LocalPreferences.allSavedAccounts()) { Amethyst.instance.okHttpClients.getHttpClient(Amethyst.instance.torManager.isSocksReady()) } - notificationManager().getOrCreateZapChannel(applicationContext) - notificationManager().getOrCreateDMChannel(applicationContext) + NotificationUtils.getOrCreateZapChannel(applicationContext) + NotificationUtils.getOrCreateDMChannel(applicationContext) } } 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/gradle/libs.versions.toml b/gradle/libs.versions.toml index ffc728e65..535ed3a99 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -40,7 +40,7 @@ navigationCompose = "2.9.3" okhttp = "5.1.0" runner = "1.7.0" rfc3986 = "0.1.2" -secp256k1KmpJniAndroid = "0.18.0" +secp256k1KmpJniAndroid = "0.19.0" securityCryptoKtx = "1.1.0" spotless = "7.2.1" torAndroid = "0.4.8.17.2" diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index ea634adf9..423db6ce9 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidLibrary) @@ -43,13 +45,20 @@ kotlin { compilerOptions { freeCompilerArgs.add("-Xstring-concat=inline") } - jvm() + jvm { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + } + } // Target declarations - add or remove as needed below. These define // which platforms this KMP module supports. // See: https://kotlinlang.org/docs/multiplatform-discover-project.html#targets androidTarget { publishLibraryVariants("release") + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + } } // For iOS targets, this is also where you should diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/INostrClient.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/INostrClient.kt new file mode 100644 index 000000000..79b6a595d --- /dev/null +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/INostrClient.kt @@ -0,0 +1,114 @@ +/** + * 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.quartz.nip01Core.relay.client + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayPool +import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +interface INostrClient { + fun relayStatusFlow(): StateFlow + + fun connect() + + fun disconnect() + + fun reconnect(onlyIfChanged: Boolean = false) + + fun isActive(): Boolean + + fun openReqSubscription( + subId: String = newSubId(), + filters: Map>, + ) + + fun openCountSubscription( + subId: String = newSubId(), + filters: Map>, + ) + + fun close(subscriptionId: String) + + fun sendIfExists( + event: Event, + connectedRelay: NormalizedRelayUrl, + ) + + fun send( + event: Event, + relayList: Set, + ) + + fun subscribe(listener: IRelayClientListener) + + fun unsubscribe(listener: IRelayClientListener) + + fun getReqFiltersOrNull(subId: String): Map>? + + fun getCountFiltersOrNull(subId: String): Map>? +} + +object EmptyNostrClient : INostrClient { + override fun relayStatusFlow() = MutableStateFlow(RelayPool.RelayPoolStatus()) + + override fun connect() { } + + override fun disconnect() { } + + override fun reconnect(onlyIfChanged: Boolean) { } + + override fun isActive() = false + + override fun openReqSubscription( + subId: String, + filters: Map>, + ) { } + + override fun openCountSubscription( + subId: String, + filters: Map>, + ) { } + + override fun close(subscriptionId: String) { } + + override fun sendIfExists( + event: Event, + connectedRelay: NormalizedRelayUrl, + ) { } + + override fun send( + event: Event, + relayList: Set, + ) { } + + override fun subscribe(listener: IRelayClientListener) {} + + override fun unsubscribe(listener: IRelayClientListener) {} + + override fun getReqFiltersOrNull(subId: String): Map>? = null + + override fun getCountFiltersOrNull(subId: String): Map>? = null +} diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt index e464d787f..70136be20 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.PoolSubscriptionRepo import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayPool import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.client.single.basic.BasicRelayClient -import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -73,7 +72,8 @@ import kotlinx.coroutines.flow.stateIn class NostrClient( private val websocketBuilder: WebsocketBuilder, private val scope: CoroutineScope, -) : IRelayClientListener { +) : INostrClient, + IRelayClientListener { private val relayPool: RelayPool = RelayPool(this, ::buildRelay) private val activeRequests: PoolSubscriptionRepository = PoolSubscriptionRepository() private val activeCounts: PoolSubscriptionRepository = PoolSubscriptionRepository() @@ -124,20 +124,20 @@ class NostrClient( } // Reconnects all relays that may have disconnected - fun connect() { + override fun connect() { isActive = true relayPool.connect() } - fun disconnect() { + override fun disconnect() { isActive = false relayPool.disconnect() } - fun isActive() = isActive + override fun isActive() = isActive @Synchronized - fun reconnect(onlyIfChanged: Boolean = false) { + override fun reconnect(onlyIfChanged: Boolean) { if (onlyIfChanged) { relayPool.reconnectIfNeedsToORIfItIsTime() } else { @@ -204,8 +204,8 @@ class NostrClient( return false } - fun openReqSubscription( - subId: String = newSubId(), + override fun openReqSubscription( + subId: String, filters: Map>, ) { val oldFilters = activeRequests.getSubscriptionFiltersOrNull(subId) ?: emptyMap() @@ -240,8 +240,8 @@ class NostrClient( } } - fun openCountSubscription( - subId: String = newSubId(), + override fun openCountSubscription( + subId: String, filters: Map>, ) { val oldFilters = activeCounts.getSubscriptionFiltersOrNull(subId) ?: emptyMap() @@ -276,7 +276,7 @@ class NostrClient( } } - fun sendIfExists( + override fun sendIfExists( event: Event, connectedRelay: NormalizedRelayUrl, ) { @@ -288,7 +288,7 @@ class NostrClient( } } - fun send( + override fun send( event: Event, relayList: Set, ) { @@ -301,7 +301,7 @@ class NostrClient( } } - fun close(subscriptionId: String) { + override fun close(subscriptionId: String) { activeRequests.remove(subscriptionId) activeCounts.remove(subscriptionId) relayPool.close(subscriptionId) @@ -386,13 +386,13 @@ class NostrClient( listeners.forEach { it.onError(relay, subId, error) } } - fun subscribe(listener: IRelayClientListener) { + override fun subscribe(listener: IRelayClientListener) { listeners = listeners.plus(listener) } fun isSubscribed(listener: IRelayClientListener): Boolean = listeners.contains(listener) - fun unsubscribe(listener: IRelayClientListener) { + override fun unsubscribe(listener: IRelayClientListener) { listeners = listeners.minus(listener) } @@ -402,7 +402,9 @@ class NostrClient( fun activeOutboxCache(url: NormalizedRelayUrl): Set = eventOutbox.activeOutboxCacheFor(url) - fun getSubscriptionFiltersOrNull(subId: String): Map>? = activeRequests.getSubscriptionFiltersOrNull(subId) + override fun getReqFiltersOrNull(subId: String): Map>? = activeRequests.getSubscriptionFiltersOrNull(subId) - fun relayStatusFlow() = relayPool.statusFlow + override fun getCountFiltersOrNull(subId: String): Map>? = activeCounts.getSubscriptionFiltersOrNull(subId) + + override fun relayStatusFlow() = relayPool.statusFlow } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClientSubscription.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClientSubscription.kt index e06da52a9..adcfeb577 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClientSubscription.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClientSubscription.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.RandomInstance class NostrClientSubscription( - val client: NostrClient, + val client: INostrClient, val filter: () -> Map> = { emptyMap() }, val onEvent: (event: Event) -> Unit = {}, ) : IRelayClientListener { diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/EventCollector.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/EventCollector.kt index 4acf6c083..79dcfdb18 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/EventCollector.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/EventCollector.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.client.accessories import android.util.Log import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient * Listens to NostrClient's onEvent messages for caching purposes. */ class EventCollector( - val client: NostrClient, + val client: INostrClient, val onEvent: (event: Event, relay: IRelayClient) -> Unit, ) { private val clientListener = diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSendAndWaitExt.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSendAndWaitExt.kt index dc80f0c89..ac499b443 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSendAndWaitExt.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSendAndWaitExt.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.client.accessories import android.util.Log import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayState import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient @@ -32,7 +32,7 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @OptIn(DelicateCoroutinesApi::class) -suspend fun NostrClient.sendAndWaitForResponse( +suspend fun INostrClient.sendAndWaitForResponse( event: Event, relayList: Set, timeoutInSeconds: Long = 15, diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSingleDownloadExt.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSingleDownloadExt.kt index 5d07db9ef..6f62fb287 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSingleDownloadExt.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSingleDownloadExt.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.client.accessories import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId @@ -32,7 +32,7 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch -fun NostrClient.downloadFirstEvent( +fun INostrClient.downloadFirstEvent( subscriptionId: String = newSubId(), filters: Map>, onResponse: (Event) -> Unit, diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayAuthenticator.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayAuthenticator.kt index d8c51d9c2..a801f84d0 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayAuthenticator.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayAuthenticator.kt @@ -21,14 +21,14 @@ package com.vitorpamplona.quartz.nip01Core.relay.client.accessories import android.util.Log -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch class RelayAuthenticator( - val client: NostrClient, + val client: INostrClient, val scope: CoroutineScope, val authenticate: suspend (challenge: String, relay: IRelayClient) -> Unit, ) { diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayInsertConfirmationCollector.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayInsertConfirmationCollector.kt index b23654bd7..6d1050acd 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayInsertConfirmationCollector.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayInsertConfirmationCollector.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.client.accessories import android.util.Log import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient * Listens to NostrClient's onEvent messages for caching purposes. */ class RelayInsertConfirmationCollector( - val client: NostrClient, + val client: INostrClient, val onRelayReceived: (eventId: HexKey, relay: IRelayClient) -> Unit, ) { private val clientListener = diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayLogger.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayLogger.kt index 40bf64eb7..039ec3916 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayLogger.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayLogger.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.client.accessories import android.util.Log import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient * Listens to NostrClient's onNotify messages from the relay */ class RelayLogger( - val client: NostrClient, + val client: INostrClient, val notify: (message: String, relay: IRelayClient) -> Unit, ) { private val clientListener = diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayNotifier.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayNotifier.kt index 289d5b95f..fe33552d2 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayNotifier.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayNotifier.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.client.accessories import android.util.Log -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient @@ -29,7 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient * Listens to NostrClient's onNotify messages from the relay */ class RelayNotifier( - val client: NostrClient, + val client: INostrClient, val notify: (message: String, relay: IRelayClient) -> Unit, ) { companion object { diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt index 6e7fd5c9e..4eb6daa85 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt @@ -94,24 +94,31 @@ class RelayPool( relay.connectAndSyncFiltersIfDisconnected() } } + updateStatus() } - fun connect() = + fun connect() { relays.forEach { url, relay -> relay.connect() } + updateStatus() + } - fun connectIfDisconnected() = + fun connectIfDisconnected() { relays.forEach { url, relay -> relay.connectAndSyncFiltersIfDisconnected() } + updateStatus() + } fun connectIfDisconnected(relay: NormalizedRelayUrl) = relays.get(relay)?.connectAndSyncFiltersIfDisconnected() - fun disconnect() = + fun disconnect() { relays.forEach { url, relay -> relay.disconnect() } + updateStatus() + } fun sendRequest( relay: NormalizedRelayUrl, diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions/SubscriptionController.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions/SubscriptionController.kt index 9e5c57d43..0f6f58aee 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions/SubscriptionController.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions/SubscriptionController.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter @@ -47,7 +47,7 @@ import com.vitorpamplona.quartz.utils.LargeCache * - Dismiss subscriptions with [dismissSubscription]. */ class SubscriptionController( - val client: NostrClient, + val client: INostrClient, ) { private val subscriptions = LargeCache() private val stats = SubscriptionStats() @@ -108,7 +108,7 @@ class SubscriptionController( fun updateRelays() { val currentFilters = subscriptions.associateWith { id, sub -> - client.getSubscriptionFiltersOrNull(id) + client.getReqFiltersOrNull(id) } subscriptions.forEach { id, sub -> diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/OtsEvent.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/OtsEvent.kt index d05879a26..5d3980940 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/OtsEvent.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/OtsEvent.kt @@ -61,17 +61,30 @@ class OtsEvent( const val KIND = 1040 const val ALT = "Opentimestamps Attestation" + /** + * Stamp is used to save OTS requests locally while we want for the bitcoin + * blockchain to include the block with the proof. + */ fun stamp( eventId: HexKey, resolver: OtsResolver, ) = resolver.stamp(eventId.hexToByteArray()) + /** + * Converts a stamp into an attestation as soon as the bitcoin chain confirms + * the block with it. If the return is not null, the attestation can be sent + * to Nostr. + */ fun upgrade( otsState: ByteArray, eventId: HexKey, resolver: OtsResolver, ) = resolver.upgrade(otsState, eventId.hexToByteArray()) + /** + * Verifies if the attestation contained in a Nostr Event is valid by checking + * with the blockchain. + */ fun verify( otsState: ByteArray, eventId: HexKey, diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/OtsResolver.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/OtsResolver.kt index 169f282e0..8dcb28aca 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/OtsResolver.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/OtsResolver.kt @@ -33,14 +33,35 @@ import com.vitorpamplona.quartz.nip03Timestamp.ots.http.CalendarPureJavaBuilder import com.vitorpamplona.quartz.nip03Timestamp.ots.op.OpSHA256 import kotlinx.coroutines.CancellationException +/** + * Resolver class for OpenTimestamps operations. + * + * This class provides functionality to stamp data with OpenTimestamps, + * upgrade existing timestamps, and verify timestamp proofs against data. + * + * @property explorer The Bitcoin explorer used for verification operations. + * @property calendarBuilder The calendar builder used for stamping operations. + */ class OtsResolver( explorer: BitcoinExplorer = BlockstreamExplorer(), calendarBuilder: CalendarBuilder = CalendarPureJavaBuilder(), ) { val ots: OpenTimestamps = OpenTimestamps(explorer, calendarBuilder) + /** + * Returns a human-readable information string about the given OTS file. + * + * @param otsState The serialized OpenTimestamps file data. + * @return A string containing information about the timestamp. + */ fun info(otsState: ByteArray): String = ots.info(DetachedTimestampFile.deserialize(otsState)) + /** + * Creates a local timestamp proof for the given data. + * + * @param data The data to be timestamped. + * @return A serialized DetachedTimestampFile containing the timestamp proof. + */ fun stamp(data: ByteArray): ByteArray { val hash = Hash(data, OpSHA256.TAG) val file = DetachedTimestampFile.from(hash) @@ -49,6 +70,13 @@ class OtsResolver( return detachedToSerialize.serialize() } + /** + * Attempts to upgrade an existing timestamp to a verified proof. + * + * @param otsState The serialized OpenTimestamps file data to upgrade. + * @param data The original data that was timestamped. + * @return A serialized DetachedTimestampFile with upgraded proof if successful, null otherwise. + */ fun upgrade( otsState: ByteArray, data: ByteArray, @@ -67,11 +95,25 @@ class OtsResolver( } } + /** + * Verifies an OpenTimestamps proof against the original data. + * + * @param otsFile The serialized OpenTimestamps file data. + * @param data The original data to verify against. + * @return VerificationState indicating the result of the verification. + */ fun verify( otsFile: ByteArray, data: ByteArray, ): VerificationState = verify(DetachedTimestampFile.deserialize(otsFile), data) + /** + * Verifies an OpenTimestamps proof against the original data. + * + * @param detachedOts The DetachedTimestampFile containing the proof. + * @param data The original data to verify against. + * @return VerificationState indicating the result of the verification. + */ fun verify( detachedOts: DetachedTimestampFile, data: ByteArray, diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/OtsResolverBuilder.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/OtsResolverBuilder.kt index 51c7f616d..502e1e78c 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/OtsResolverBuilder.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/OtsResolverBuilder.kt @@ -23,3 +23,7 @@ package com.vitorpamplona.quartz.nip03Timestamp interface OtsResolverBuilder { fun build(): OtsResolver } + +class DefaultOtsResolverBuilder : OtsResolverBuilder { + override fun build(): OtsResolver = OtsResolver() +} 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) } } } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/ots/http/BlockstreamExplorer.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/ots/http/BlockstreamExplorer.kt index e024986bb..f2f5b7d36 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/ots/http/BlockstreamExplorer.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/ots/http/BlockstreamExplorer.kt @@ -26,26 +26,6 @@ import com.vitorpamplona.quartz.nip03Timestamp.ots.BlockHeader import java.net.URL import java.util.concurrent.Executors -/** - * 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. - */ class BlockstreamExplorer : BitcoinExplorer { /** * Retrieve the block information from the block hash. diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/ots/http/Request.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/ots/http/Request.kt index 341b5a0d4..025d4d7c1 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/ots/http/Request.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/ots/http/Request.kt @@ -50,8 +50,8 @@ class Request( try { val httpURLConnection = url.openConnection() as HttpURLConnection - httpURLConnection.setReadTimeout(10000) - httpURLConnection.setConnectTimeout(10000) + httpURLConnection.readTimeout = 10000 + httpURLConnection.connectTimeout = 10000 httpURLConnection.setRequestProperty("User-Agent", "OpenTimestamps Java") httpURLConnection.setRequestProperty("Accept", "application/json") httpURLConnection.setRequestProperty("Accept-Encoding", "gzip") @@ -62,17 +62,17 @@ class Request( if (data != null) { httpURLConnection.setDoOutput(true) - httpURLConnection.setRequestMethod("POST") + httpURLConnection.requestMethod = "POST" httpURLConnection.setRequestProperty( "Content-Length", "" + this.data!!.size.toString(), ) - val wr = DataOutputStream(httpURLConnection.getOutputStream()) - wr.write(this.data, 0, this.data!!.size) - wr.flush() - wr.close() + DataOutputStream(httpURLConnection.getOutputStream()).use { wr -> + wr.write(this.data, 0, this.data!!.size) + wr.flush() + } } else { - httpURLConnection.setRequestMethod("GET") + httpURLConnection.requestMethod = "GET" } httpURLConnection.connect() @@ -84,7 +84,7 @@ class Request( response.status = responseCode response.fromUrl = url.toString() var `is` = httpURLConnection.getInputStream() - if ("gzip" == httpURLConnection.getContentEncoding()) { + if ("gzip" == httpURLConnection.contentEncoding) { `is` = GZIPInputStream(`is`) } response.setStream(`is`) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/ots/http/Response.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/ots/http/Response.kt index dc44a417f..403f8bbfc 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/ots/http/Response.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/ots/http/Response.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip03Timestamp.ots.http import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.json.JsonMapper import java.io.ByteArrayOutputStream +import java.io.Closeable import java.io.IOException import java.io.InputStream import java.nio.charset.StandardCharsets @@ -30,7 +31,7 @@ import java.nio.charset.StandardCharsets /** * Holds the response from an HTTP request. */ -class Response { +class Response : Closeable { private var stream: InputStream? = null var fromUrl: String? = null @@ -53,7 +54,7 @@ class Response { @get:Throws(IOException::class) val string: String - get() = kotlin.text.String(this.bytes, StandardCharsets.UTF_8) + get() = String(this.bytes, StandardCharsets.UTF_8) @get:Throws(IOException::class) val bytes: ByteArray @@ -79,4 +80,9 @@ class Response { JsonMapper.builder().build() return builder.readTree(jsonString) } + + override fun close() { + stream?.close() + stream = null + } } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/ots/op/OpCrypto.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/ots/op/OpCrypto.kt index a54907c44..9675a8fe1 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/ots/op/OpCrypto.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/ots/op/OpCrypto.kt @@ -62,7 +62,7 @@ abstract class OpCrypto internal constructor() : OpUnary() { val digest = MessageDigest.getInstance(this.hashLibName()) var chunk = ctx.read(1048576) - while (chunk != null && chunk.size > 0) { + while (chunk.isNotEmpty()) { digest.update(chunk) chunk = ctx.read(1048576) } @@ -73,7 +73,7 @@ abstract class OpCrypto internal constructor() : OpUnary() { } @Throws(IOException::class, NoSuchAlgorithmException::class) - fun hashFd(file: File?): ByteArray = hashFd(FileInputStream(file)) + fun hashFd(file: File?): ByteArray = FileInputStream(file).use { inputStream -> hashFd(inputStream) } @Throws(IOException::class, NoSuchAlgorithmException::class) fun hashFd(bytes: ByteArray): ByteArray { @@ -93,6 +93,7 @@ abstract class OpCrypto internal constructor() : OpUnary() { count = inputStream.read(chunk, 0, 1048576) } + // TODO: Is this needed? Closing of stream should be callers responsibility? inputStream.close() val hash = digest.digest() diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt index 35b3474a2..fec2ca5ad 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt @@ -34,6 +34,9 @@ class DecryptZapResponse { ) fun parse(intent: IntentResult): SignerResult.RequestAddressed { + if (intent.rejected) { + return SignerResult.RequestAddressed.ManuallyRejected() + } val eventJson = intent.result return if (!eventJson.isNullOrBlank()) { if (eventJson.startsWith("{")) { diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DeriveKeyResponse.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DeriveKeyResponse.kt index 3d51fb9a5..c1353a1e6 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DeriveKeyResponse.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DeriveKeyResponse.kt @@ -33,6 +33,9 @@ class DeriveKeyResponse { ) fun parse(intent: IntentResult): SignerResult.RequestAddressed { + if (intent.rejected) { + return SignerResult.RequestAddressed.ManuallyRejected() + } val newPrivateKey = intent.result return if (newPrivateKey != null) { SignerResult.RequestAddressed.Successful(DerivationResult(newPrivateKey)) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04DecryptResponse.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04DecryptResponse.kt index 84cc20ab6..ee94be069 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04DecryptResponse.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04DecryptResponse.kt @@ -32,6 +32,9 @@ class Nip04DecryptResponse { ) fun parse(intent: IntentResult): SignerResult.RequestAddressed { + if (intent.rejected) { + return SignerResult.RequestAddressed.ManuallyRejected() + } val plaintext = intent.result return if (plaintext != null) { SignerResult.RequestAddressed.Successful(DecryptionResult(plaintext)) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04EncryptResponse.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04EncryptResponse.kt index 969520fef..874f782e0 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04EncryptResponse.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04EncryptResponse.kt @@ -32,6 +32,10 @@ class Nip04EncryptResponse { ) fun parse(intent: IntentResult): SignerResult.RequestAddressed { + if (intent.rejected) { + return SignerResult.RequestAddressed.ManuallyRejected() + } + val ciphertext = intent.result return if (ciphertext != null) { SignerResult.RequestAddressed.Successful(EncryptionResult(ciphertext)) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44DecryptResponse.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44DecryptResponse.kt index e65dd8f4f..2f609f0e9 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44DecryptResponse.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44DecryptResponse.kt @@ -32,6 +32,9 @@ class Nip44DecryptResponse { ) fun parse(intent: IntentResult): SignerResult.RequestAddressed { + if (intent.rejected) { + return SignerResult.RequestAddressed.ManuallyRejected() + } val plaintext = intent.result return if (plaintext != null) { SignerResult.RequestAddressed.Successful(DecryptionResult(plaintext)) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44EncryptResponse.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44EncryptResponse.kt index 243856a38..fc5615442 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44EncryptResponse.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44EncryptResponse.kt @@ -32,6 +32,9 @@ class Nip44EncryptResponse { ) fun parse(intent: IntentResult): SignerResult.RequestAddressed { + if (intent.rejected) { + return SignerResult.RequestAddressed.ManuallyRejected() + } val ciphertext = intent.result return if (ciphertext != null) { SignerResult.RequestAddressed.Successful(EncryptionResult(ciphertext)) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/SignResponse.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/SignResponse.kt index 6e8a6d639..d37fb0d9a 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/SignResponse.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/SignResponse.kt @@ -39,6 +39,10 @@ class SignResponse { intent: IntentResult, unsignedEvent: Event, ): SignerResult.RequestAddressed { + if (intent.rejected) { + return SignerResult.RequestAddressed.ManuallyRejected() + } + val eventJson = intent.event return if (eventJson != null) { if (eventJson.startsWith("{")) { diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResult.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResult.kt index b1beca7a8..58844729b 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResult.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResult.kt @@ -29,6 +29,7 @@ data class IntentResult( val result: String? = null, val event: String? = null, val id: String? = null, + val rejected: Boolean = false, ) { fun toJson(): String = JsonMapper.mapper.writeValueAsString(this) @@ -48,6 +49,7 @@ data class IntentResult( result = data.getStringExtra("result"), event = data.getStringExtra("event"), `package` = data.getStringExtra("package"), + rejected = data.extras?.containsKey("rejected") == true, ) fun fromJson(json: String): IntentResult = JsonMapper.mapper.readValue(json) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResultJsonDeserializer.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResultJsonDeserializer.kt index e53cb8be2..7b21edf39 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResultJsonDeserializer.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResultJsonDeserializer.kt @@ -36,6 +36,7 @@ class IntentResultJsonDeserializer : StdDeserializer(IntentResult: result = jsonObject.get("result")?.asText()?.intern(), event = jsonObject.get("event")?.asText()?.intern(), id = jsonObject.get("id")?.asText()?.intern(), + rejected = jsonObject.get("rejected")?.asBoolean() ?: false, ) } } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResultJsonSerializer.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResultJsonSerializer.kt index 289f4daed..d8e5f9226 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResultJsonSerializer.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResultJsonSerializer.kt @@ -35,6 +35,7 @@ class IntentResultJsonSerializer : StdSerializer(IntentResult::cla result.result?.let { gen.writeStringField("result", it) } result.event?.let { gen.writeStringField("event", it) } result.id?.let { gen.writeStringField("id", it) } + result.rejected.let { gen.writeBooleanField("rejected", it) } gen.writeEndObject() } } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/TimeUtils.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/TimeUtils.kt index d271d7b8a..0bd1ceec2 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/TimeUtils.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/TimeUtils.kt @@ -64,4 +64,6 @@ object TimeUtils { fun randomWithTwoDays() = now() - RandomInstance.int(twoDays()) fun ninetyDaysFromNow() = now() + NINETY_DAYS + + fun oneYearAgo() = now() - ONE_YEAR }