- Separates Application dependencies into an AppModules class to create only after the OnCreate event.
- Switches TorSettings to be per Application and not per Account anymore - Since TorSettings is now global, moves the okHttpClient determinations out of the Account-based classes into the Application. - Since TorSettings is now global, set's up Coil's image loader only once when creating the Application - Moves UISettings state to App Modules instead of viewmodel - Migrates TorSettings and UISettings to DataStore - Accounts now have their own coroutine scopes to allow cancellation when they are unloaded/logged off - New tor evaluator service for relay connections now uses all account's trusted relays and dm relays at the same time. - Migrates composable-state-based UISettings to Flow-based UI settings, while observing connectivity status - Removes the displayFeatures and windowSizeClass from the shared model - Fixes not requesting Notification Permissions for APIs older than Tiramisu in the FDroid flavor - Moves the NIP-11 document cache from singleton to the App Modules - Avoids using AccountViewModel to check NIP-11 Relay documents - Moves the UI Settings usage in composables to functions that do not observe the state since they don't need to refresh the screen when changed. - Refactors UI Settings screen to separate components and remove the sharedViewModel - Only starts Internal Tor if that option is selected in the TorSettings. - Turn TorSettings into a data class to observe changes to it - Drops the SharedPreferences ViewModel to use UISettingsFlow directly from App Modules - Reorganizes OTS Events after simplification of the OkHttp based on TorSettings.
This commit is contained in:
+36
-13
@@ -20,6 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -36,6 +38,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -44,8 +47,10 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
|
||||
import com.halilibo.richtext.commonmark.MarkdownParseOptions
|
||||
import com.halilibo.richtext.markdown.BasicMarkdown
|
||||
@@ -53,11 +58,10 @@ import com.halilibo.richtext.ui.RichTextStyle
|
||||
import com.halilibo.richtext.ui.material3.RichText
|
||||
import com.halilibo.richtext.ui.resolveDefaults
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
||||
import com.vitorpamplona.amethyst.service.notifications.PushDistributorHandler
|
||||
import com.vitorpamplona.amethyst.ui.components.SpinnerSelectionDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.checkifItNeedsToRequestNotificationPermission
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -65,12 +69,31 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesViewModel) {
|
||||
val notificationPermissionState =
|
||||
checkifItNeedsToRequestNotificationPermission(sharedPreferencesViewModel)
|
||||
fun SelectNotificationProvider(sharedPrefs: UiSettingsFlow) {
|
||||
val notificationPermissionGranted =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val notificationPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
||||
|
||||
if (notificationPermissionState.status.isGranted) {
|
||||
if (!sharedPreferencesViewModel.sharedPrefs.dontShowPushNotificationSelector) {
|
||||
if (!notificationPermissionState.status.isGranted) {
|
||||
val dontAskForNotificationPermissions by sharedPrefs.dontAskForNotificationPermissions.collectAsStateWithLifecycle()
|
||||
if (!dontAskForNotificationPermissions) {
|
||||
sharedPrefs.dontAskForNotificationPermissions()
|
||||
|
||||
// This will pause the APP, including the connection with relays.
|
||||
LaunchedEffect(notificationPermissionState) {
|
||||
notificationPermissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notificationPermissionState.status.isGranted
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
if (notificationPermissionGranted) {
|
||||
val pushNotificationSelector by sharedPrefs.dontShowPushNotificationSelector.collectAsStateWithLifecycle()
|
||||
if (!pushNotificationSelector) {
|
||||
val context = LocalContext.current
|
||||
var distributorPresent by remember {
|
||||
mutableStateOf(PushDistributorHandler.savedDistributorExists())
|
||||
@@ -84,8 +107,8 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
|
||||
onSelect = { index ->
|
||||
if (list[index] == "None") {
|
||||
PushDistributorHandler.forceRemoveDistributor(context)
|
||||
sharedPreferencesViewModel.dontAskForNotificationPermissions()
|
||||
sharedPreferencesViewModel.dontShowPushNotificationSelector()
|
||||
sharedPrefs.dontAskForNotificationPermissions()
|
||||
sharedPrefs.dontShowPushNotificationSelector()
|
||||
} else {
|
||||
val fullDistributorName = list[index]
|
||||
PushDistributorHandler.saveDistributor(fullDistributorName)
|
||||
@@ -125,7 +148,7 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
|
||||
TextButton(
|
||||
onClick = {
|
||||
distributorPresent = true
|
||||
sharedPreferencesViewModel.dontShowPushNotificationSelector()
|
||||
sharedPrefs.dontShowPushNotificationSelector()
|
||||
},
|
||||
) {
|
||||
Text(stringRes(R.string.quick_action_dont_show_again_button))
|
||||
@@ -187,7 +210,7 @@ fun LoadDistributors(onInner: @Composable (String, ImmutableList<String>, Immuta
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PushNotificationSettingsRow(sharedPreferencesViewModel: SharedPreferencesViewModel) {
|
||||
fun PushNotificationSettingsRow(sharedPrefs: UiSettingsFlow) {
|
||||
val context = LocalContext.current
|
||||
|
||||
LoadDistributors { currentDistributor, list, readableListWithExplainer ->
|
||||
@@ -198,8 +221,8 @@ fun PushNotificationSettingsRow(sharedPreferencesViewModel: SharedPreferencesVie
|
||||
selectedIndex = list.indexOf(currentDistributor),
|
||||
) { index ->
|
||||
if (list[index] == "None") {
|
||||
sharedPreferencesViewModel.dontAskForNotificationPermissions()
|
||||
sharedPreferencesViewModel.dontShowPushNotificationSelector()
|
||||
sharedPrefs.dontAskForNotificationPermissions()
|
||||
sharedPrefs.dontShowPushNotificationSelector()
|
||||
PushDistributorHandler.forceRemoveDistributor(context)
|
||||
} else {
|
||||
PushDistributorHandler.saveDistributor(list[index])
|
||||
|
||||
@@ -21,210 +21,33 @@
|
||||
package com.vitorpamplona.amethyst
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ContentResolver
|
||||
import android.util.Log
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import coil3.disk.DiskCache
|
||||
import coil3.memory.MemoryCache
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState
|
||||
import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager
|
||||
import com.vitorpamplona.amethyst.service.crashreports.CrashReportCache
|
||||
import com.vitorpamplona.amethyst.service.crashreports.UnexpectedCrashSaver
|
||||
import com.vitorpamplona.amethyst.service.eventCache.MemoryTrimmingService
|
||||
import com.vitorpamplona.amethyst.service.images.ImageCacheFactory
|
||||
import com.vitorpamplona.amethyst.service.images.ImageLoaderSetup
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.service.logging.Logging
|
||||
import com.vitorpamplona.amethyst.service.notifications.PokeyReceiver
|
||||
import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager
|
||||
import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache
|
||||
import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket
|
||||
import com.vitorpamplona.amethyst.service.okhttp.ProxySettingsAnchor
|
||||
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache
|
||||
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCacheFactory
|
||||
import com.vitorpamplona.amethyst.service.relayClient.CacheClientConnector
|
||||
import com.vitorpamplona.amethyst.service.relayClient.RelayProxyClientConnector
|
||||
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.AuthCoordinator
|
||||
import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.NotifyCoordinator
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator
|
||||
import com.vitorpamplona.amethyst.service.relayClient.speedLogger.RelaySpeedLogger
|
||||
import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.scope
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorManager
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OtsBlockHeightCache
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
class Amethyst : Application() {
|
||||
val appAgent = "Amethyst/${BuildConfig.VERSION_NAME}"
|
||||
|
||||
// Exists to avoid exceptions stopping the coroutine
|
||||
val exceptionHandler =
|
||||
CoroutineExceptionHandler { _, throwable ->
|
||||
Log.e("AmethystCoroutine", "Caught exception: ${throwable.message}", throwable)
|
||||
}
|
||||
|
||||
val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler)
|
||||
|
||||
// Key cache service to download and decrypt encrypted files before caching them.
|
||||
val keyCache = EncryptionKeyCache()
|
||||
|
||||
// App services that should be run as soon as there are subscribers to their flows
|
||||
val locationManager = LocationState(this, applicationIOScope)
|
||||
val connManager = ConnectivityManager(this, applicationIOScope)
|
||||
|
||||
val torManager = TorManager(this, applicationIOScope)
|
||||
|
||||
// Service that will run at all times to receive events from Pokey
|
||||
val pokeyReceiver = PokeyReceiver()
|
||||
|
||||
// manages all the other connections separately from relays.
|
||||
val okHttpClients =
|
||||
DualHttpClientManager(
|
||||
userAgent = appAgent,
|
||||
proxyPortProvider = torManager.activePortOrNull,
|
||||
isMobileDataProvider = connManager.isMobileOrNull,
|
||||
keyCache = keyCache,
|
||||
scope = applicationIOScope,
|
||||
)
|
||||
|
||||
// manages all relay connections
|
||||
val okHttpClientForRelays =
|
||||
DualHttpClientManager(
|
||||
userAgent = appAgent,
|
||||
proxyPortProvider = torManager.activePortOrNull,
|
||||
isMobileDataProvider = connManager.isMobileOrNull,
|
||||
keyCache = keyCache,
|
||||
scope = applicationIOScope,
|
||||
)
|
||||
|
||||
// manages all relay connections
|
||||
val okHttpClientForRelaysForDms =
|
||||
DualHttpClientManager(
|
||||
userAgent = appAgent,
|
||||
proxyPortProvider = torManager.activePortOrNull,
|
||||
isMobileDataProvider = connManager.isMobileOrNull,
|
||||
keyCache = keyCache,
|
||||
scope = applicationIOScope,
|
||||
)
|
||||
|
||||
val torProxySettingsAnchor = ProxySettingsAnchor()
|
||||
|
||||
// Connects the NostrClient class with okHttp
|
||||
val websocketBuilder =
|
||||
OkHttpWebSocket.Builder { url ->
|
||||
val useTor = torProxySettingsAnchor.useProxy(url)
|
||||
if (torProxySettingsAnchor.isDM(url)) {
|
||||
okHttpClientForRelaysForDms.getHttpClient(useTor)
|
||||
} else {
|
||||
okHttpClientForRelays.getHttpClient(useTor)
|
||||
}
|
||||
}
|
||||
|
||||
// Caches all events in Memory
|
||||
val cache: LocalCache = LocalCache
|
||||
|
||||
// Organizes cache clearing
|
||||
val trimmingService = MemoryTrimmingService(cache)
|
||||
|
||||
// Provides a relay pool
|
||||
val client: INostrClient = NostrClient(websocketBuilder, applicationIOScope)
|
||||
|
||||
// Watches for changes on Tor and Relay List Settings
|
||||
val relayProxyClientConnector = RelayProxyClientConnector(torProxySettingsAnchor, okHttpClients, connManager, client, torManager, applicationIOScope)
|
||||
|
||||
// Verifies and inserts in the cache from all relays, all subscriptions
|
||||
val cacheClientConnector = CacheClientConnector(client, cache)
|
||||
|
||||
// Show messages from the Relay and controls their dismissal
|
||||
val notifyCoordinator = NotifyCoordinator(client)
|
||||
|
||||
// Authenticates with relays.
|
||||
val authCoordinator = AuthCoordinator(client, applicationIOScope)
|
||||
|
||||
val logger = if (isDebug) RelaySpeedLogger(client) else null
|
||||
|
||||
// Coordinates all subscriptions for the Nostr Client
|
||||
val sources: RelaySubscriptionsCoordinator = RelaySubscriptionsCoordinator(LocalCache, client, applicationIOScope)
|
||||
|
||||
// keeps all accounts live
|
||||
val accountsCache =
|
||||
AccountCacheState(
|
||||
geolocationFlow = locationManager.geohashStateFlow,
|
||||
nwcFilterAssembler = sources.nwc,
|
||||
contentResolverFn = ::contentResolverFn,
|
||||
cache = cache,
|
||||
client = client,
|
||||
)
|
||||
|
||||
// saves the .content of NIP-95 blobs in disk to save memory
|
||||
val nip95cache: File by lazy { Nip95CacheFactory.new(this) }
|
||||
|
||||
// local video cache with disk + memory
|
||||
val videoCache: VideoCache by lazy { VideoCacheFactory.new(this) }
|
||||
|
||||
// image cache in disk for coil
|
||||
val diskCache: DiskCache by lazy { ImageCacheFactory.newDisk(this) }
|
||||
|
||||
// image cache in memory for coil
|
||||
val memoryCache: MemoryCache by lazy { ImageCacheFactory.newMemory(this) }
|
||||
|
||||
val crashReportCache: CrashReportCache by lazy { CrashReportCache(this) }
|
||||
|
||||
// Application-wide ots verification cache
|
||||
val otsVerifCache by lazy { VerificationStateCache() }
|
||||
|
||||
// Application-wide block height request cache
|
||||
val otsBlockHeightCache by lazy { OtsBlockHeightCache() }
|
||||
companion object {
|
||||
lateinit var instance: AppModules
|
||||
private set
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d("AmethystApp", "onCreate $this")
|
||||
instance = this
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler(UnexpectedCrashSaver(crashReportCache, applicationIOScope))
|
||||
instance = AppModules(this)
|
||||
|
||||
if (isDebug) {
|
||||
Logging.setup()
|
||||
}
|
||||
|
||||
// initializes diskcache on an IO thread.
|
||||
applicationIOScope.launch {
|
||||
diskCache
|
||||
videoCache
|
||||
}
|
||||
|
||||
// registers to receive events
|
||||
pokeyReceiver.register(this)
|
||||
instance.initiate(this)
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
super.onTerminate()
|
||||
Log.d("AmethystApp", "onTerminate $this")
|
||||
|
||||
pokeyReceiver.unregister(this)
|
||||
applicationIOScope.cancel("Application onTerminate $this")
|
||||
instance.terminate(this)
|
||||
}
|
||||
|
||||
fun contentResolverFn(): ContentResolver = contentResolver
|
||||
|
||||
fun setImageLoader(shouldUseTor: (String) -> Boolean?) {
|
||||
ImageLoaderSetup.setup(this, diskCache, memoryCache) { url ->
|
||||
shouldUseTor(url)?.let { okHttpClients.getHttpClient(it) } ?: okHttpClients.getHttpClient(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences = EncryptedStorage.preferences(instance, npub)
|
||||
|
||||
/**
|
||||
* Release memory when the UI becomes hidden or when system resources become low.
|
||||
*
|
||||
@@ -233,13 +56,6 @@ class Amethyst : Application() {
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
Log.d("AmethystApp", "onTrimMemory $level")
|
||||
applicationIOScope.launch(Dispatchers.Default) {
|
||||
trimmingService.run(null, LocalPreferences.allSavedAccounts())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var instance: Amethyst
|
||||
private set
|
||||
instance.trim()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,12 +30,10 @@ import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.amethyst.model.ALL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.Settings
|
||||
import com.vitorpamplona.amethyst.model.UiSettings
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
@@ -210,7 +208,7 @@ object LocalPreferences {
|
||||
}
|
||||
|
||||
private val prefsDirPath: String
|
||||
get() = "${Amethyst.instance.filesDir.parent}/shared_prefs/"
|
||||
get() = "${Amethyst.instance.appContext.filesDir.parent}/shared_prefs/"
|
||||
|
||||
private suspend fun addAccount(accInfo: AccountInfo) {
|
||||
val accounts = savedAccounts().filter { it.npub != accInfo.npub }.plus(accInfo)
|
||||
@@ -255,7 +253,7 @@ object LocalPreferences {
|
||||
return if (BuildConfig.DEBUG && DEBUG_PLAINTEXT_PREFERENCES) {
|
||||
val preferenceFile =
|
||||
if (npub == null) DEBUG_PREFERENCES_NAME else "${DEBUG_PREFERENCES_NAME}_$npub"
|
||||
Amethyst.instance.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE)
|
||||
Amethyst.instance.appContext.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE)
|
||||
} else {
|
||||
Amethyst.instance.encryptedStorage(npub)
|
||||
}
|
||||
@@ -475,8 +473,6 @@ object LocalPreferences {
|
||||
remove(PrefKeys.USE_PROXY)
|
||||
remove(PrefKeys.PROXY_PORT)
|
||||
|
||||
putString(PrefKeys.TOR_SETTINGS, JsonMapper.mapper.writeValueAsString(settings.torSettings.toSettings()))
|
||||
|
||||
val regularMap =
|
||||
settings.lastReadPerRoute.value.mapValues {
|
||||
it.value.value
|
||||
@@ -501,7 +497,7 @@ object LocalPreferences {
|
||||
suspend fun loadAccountConfigFromEncryptedStorage(): AccountSettings? = currentAccount()?.let { loadAccountConfigFromEncryptedStorage(it) }
|
||||
|
||||
suspend fun saveSharedSettings(
|
||||
sharedSettings: Settings,
|
||||
sharedSettings: UiSettings,
|
||||
prefs: SharedPreferences = encryptedPreferences(),
|
||||
) {
|
||||
Log.d("LocalPreferences", "Saving to shared settings")
|
||||
@@ -510,11 +506,13 @@ object LocalPreferences {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadSharedSettings(prefs: SharedPreferences = encryptedPreferences()): Settings? {
|
||||
suspend fun loadSharedSettings(prefs: SharedPreferences = encryptedPreferences()): UiSettings? {
|
||||
Log.d("LocalPreferences", "Load shared settings")
|
||||
with(prefs) {
|
||||
return try {
|
||||
getString(PrefKeys.SHARED_SETTINGS, "{}")?.let { JsonMapper.mapper.readValue<Settings>(it) }
|
||||
getString(PrefKeys.SHARED_SETTINGS, "{}")?.let {
|
||||
JsonMapper.mapper.readValue<UiSettings>(it)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w(
|
||||
@@ -598,8 +596,6 @@ object LocalPreferences {
|
||||
val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false)
|
||||
val hideNIP17WarningDialog = getBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, false)
|
||||
|
||||
val torSettings = parseOrNull<TorSettings>(PrefKeys.TOR_SETTINGS) ?: TorSettings()
|
||||
|
||||
val lastReadPerRoute =
|
||||
parseOrNull<Map<String, Long>>(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),
|
||||
|
||||
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.model
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
@@ -80,7 +79,6 @@ import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState
|
||||
import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState
|
||||
import com.vitorpamplona.amethyst.model.nip96FileStorage.FileStorageServerListState
|
||||
import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState
|
||||
import com.vitorpamplona.amethyst.model.privacyOptions.PrivacyState
|
||||
import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState
|
||||
import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineRelayListsState
|
||||
import com.vitorpamplona.amethyst.model.serverList.MergedServerListState
|
||||
@@ -89,7 +87,6 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.FeedDecryptionCaches
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.FeedTopNavFilterState
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxLoaderState
|
||||
import com.vitorpamplona.amethyst.model.torState.TorRelayState
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
|
||||
import com.vitorpamplona.amethyst.service.uploads.FileHeader
|
||||
@@ -136,8 +133,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.references.references
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OkHttpOtsResolverBuilder
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
|
||||
import com.vitorpamplona.quartz.nip04Dm.PrivateDMCache
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||
@@ -226,7 +222,7 @@ class Account(
|
||||
val signer: NostrSigner,
|
||||
val geolocationFlow: StateFlow<LocationState.LocationResult>,
|
||||
val nwcFilterAssembler: NWCPaymentFilterAssembler,
|
||||
val otsVerifCache: VerificationStateCache,
|
||||
val otsResolverBuilder: OtsResolverBuilder,
|
||||
val cache: LocalCache,
|
||||
val client: INostrClient,
|
||||
val scope: CoroutineScope,
|
||||
@@ -323,17 +319,6 @@ class Account(
|
||||
|
||||
val chatroomList = cache.getOrCreateChatroomList(signer.pubKey)
|
||||
|
||||
val privacyState = PrivacyState(settings)
|
||||
val torRelayState = TorRelayState(trustedRelays, dmRelayList, settings, scope)
|
||||
|
||||
val otsResolverBuilder: OkHttpOtsResolverBuilder =
|
||||
OkHttpOtsResolverBuilder(
|
||||
{
|
||||
Amethyst.instance.okHttpClients.getHttpClient(privacyState.shouldUseTorForMoneyOperations(it))
|
||||
},
|
||||
privacyState::shouldUseTorForMoneyOperations,
|
||||
Amethyst.instance.otsBlockHeightCache,
|
||||
)
|
||||
val newNotesPreProcessor = EventProcessor(this, cache)
|
||||
|
||||
val otsState = OtsState(signer, cache, otsResolverBuilder, scope, settings)
|
||||
|
||||
@@ -24,8 +24,6 @@ import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
@@ -132,7 +130,6 @@ class AccountSettings(
|
||||
var backupHashtagList: HashtagListEvent? = null,
|
||||
var backupGeohashList: GeohashListEvent? = null,
|
||||
var backupEphemeralChatList: EphemeralChatListEvent? = null,
|
||||
val torSettings: TorSettingsFlow = TorSettingsFlow(),
|
||||
val lastReadPerRoute: MutableStateFlow<Map<String, MutableStateFlow<Long>>> = MutableStateFlow(mapOf()),
|
||||
var hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf<String>()),
|
||||
val pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
|
||||
@@ -249,17 +246,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
// proxy settings
|
||||
// ---
|
||||
fun setTorSettings(newTorSettings: TorSettings): Boolean =
|
||||
if (torSettings.update(newTorSettings)) {
|
||||
saveAccountSettings()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
// ---
|
||||
// language services
|
||||
// ---
|
||||
|
||||
+1
-1
@@ -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,
|
||||
@@ -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<ThemeType> = MutableStateFlow(ThemeType.SYSTEM),
|
||||
val preferredLanguage: MutableStateFlow<String?> = MutableStateFlow(null),
|
||||
val automaticallyShowImages: MutableStateFlow<ConnectivityType> = MutableStateFlow(ConnectivityType.ALWAYS),
|
||||
val automaticallyStartPlayback: MutableStateFlow<ConnectivityType> = MutableStateFlow(ConnectivityType.ALWAYS),
|
||||
val automaticallyShowUrlPreview: MutableStateFlow<ConnectivityType> = MutableStateFlow(ConnectivityType.ALWAYS),
|
||||
val automaticallyHideNavigationBars: MutableStateFlow<BooleanType> = MutableStateFlow(BooleanType.ALWAYS),
|
||||
val automaticallyShowProfilePictures: MutableStateFlow<ConnectivityType> = MutableStateFlow(ConnectivityType.ALWAYS),
|
||||
val dontShowPushNotificationSelector: MutableStateFlow<Boolean> = MutableStateFlow(false),
|
||||
val dontAskForNotificationPermissions: MutableStateFlow<Boolean> = MutableStateFlow(false),
|
||||
val featureSet: MutableStateFlow<FeatureSetType> = MutableStateFlow(FeatureSetType.SIMPLIFIED),
|
||||
val gallerySet: MutableStateFlow<ProfileGalleryType> = 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -32,7 +32,7 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -47,6 +47,7 @@ class AccountCacheState(
|
||||
val geolocationFlow: StateFlow<LocationState.LocationResult>,
|
||||
val nwcFilterAssembler: NWCPaymentFilterAssembler,
|
||||
val contentResolverFn: () -> ContentResolver,
|
||||
val otsResolverBuilder: OtsResolverBuilder,
|
||||
val cache: LocalCache,
|
||||
val client: INostrClient,
|
||||
) {
|
||||
@@ -91,7 +92,7 @@ class AccountCacheState(
|
||||
signer = signer,
|
||||
geolocationFlow = geolocationFlow,
|
||||
nwcFilterAssembler = nwcFilterAssembler,
|
||||
otsVerifCache = VerificationStateCache(),
|
||||
otsResolverBuilder = otsResolverBuilder,
|
||||
cache = cache,
|
||||
client = client,
|
||||
scope =
|
||||
|
||||
+52
@@ -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<Nip11RelayInformation> = loadRelayInfo(relay, Amethyst.instance.nip11Cache)
|
||||
|
||||
@Composable
|
||||
fun loadRelayInfo(
|
||||
relay: NormalizedRelayUrl,
|
||||
nip11Cache: Nip11CachedRetriever,
|
||||
): State<Nip11RelayInformation> =
|
||||
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")
|
||||
},
|
||||
)
|
||||
}
|
||||
+9
-99
@@ -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<NormalizedRelayUrl, Nip11RelayInformation>(1000)
|
||||
private val relayInformationDocumentCache = LruCache<NormalizedRelayUrl, RetrieveResult?>(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+90
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+49
@@ -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()
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.model.UiSettings
|
||||
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
val Context.sharedPreferencesDataStore: DataStore<Preferences> 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<UiSettings>(it) }
|
||||
|
||||
if (newVersion != null) {
|
||||
newVersion
|
||||
} else {
|
||||
val oldVersion = LocalPreferences.loadSharedSettings()
|
||||
if (oldVersion != null) {
|
||||
save(oldVersion)
|
||||
}
|
||||
oldVersion
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Log any errors that occur while reading the DataStore.
|
||||
Log.e("SharedPreferences", "Error reading DataStore preferences: ${e.message}")
|
||||
null
|
||||
}
|
||||
|
||||
suspend fun save(sharedSettings: UiSettings) {
|
||||
try {
|
||||
val str = JsonMapper.mapper.writeValueAsString(sharedSettings)
|
||||
context.sharedPreferencesDataStore.edit { preferences ->
|
||||
preferences[UI_SETTINGS] = str
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Log any errors that occur while reading the DataStore.
|
||||
Log.e("SharedPreferences", "Error saving DataStore preferences: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class TorSharedPreferences(
|
||||
val context: Context,
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
companion object {
|
||||
val TOR_SETTINGS = stringPreferencesKey("tor_settings")
|
||||
}
|
||||
|
||||
// Tor Preferences. Makes sure to wait for it to avoid connecting with random IPs
|
||||
val value =
|
||||
runBlocking {
|
||||
TorSettingsFlow.build(torPreferences() ?: TorSettings())
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val saving =
|
||||
value.propertyWatchFlow
|
||||
.debounce(1000)
|
||||
.distinctUntilChanged()
|
||||
.onEach(::save)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
value.toSettings(),
|
||||
)
|
||||
|
||||
suspend fun torPreferences(): TorSettings? =
|
||||
try {
|
||||
// Get the preference flow and take the first value.
|
||||
val preferences = context.sharedPreferencesDataStore.data.first()
|
||||
println("AABBCC Loading TorSettings: ${preferences[TOR_SETTINGS]}")
|
||||
preferences[TOR_SETTINGS]?.let {
|
||||
println("AABBCC Loading TorSettings: $it")
|
||||
JsonMapper.mapper.readValue<TorSettings>(it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Log any errors that occur while reading the DataStore.
|
||||
Log.e("SharedPreferences", "Error reading DataStore preferences: ${e.message}")
|
||||
null
|
||||
}
|
||||
|
||||
suspend fun save(torSettings: TorSettings) {
|
||||
try {
|
||||
println("AABBCC Saving TorSettings: $torSettings")
|
||||
val str = JsonMapper.mapper.writeValueAsString(torSettings)
|
||||
context.sharedPreferencesDataStore.edit { preferences ->
|
||||
preferences[TOR_SETTINGS] = str
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Log any errors that occur while reading the DataStore.
|
||||
Log.e("SharedPreferences", "Error saving DataStore preferences: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -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
|
||||
}
|
||||
+41
@@ -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
|
||||
}
|
||||
+47
-28
@@ -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())
|
||||
}
|
||||
+44
@@ -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
|
||||
}
|
||||
+101
@@ -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<Set<NormalizedRelayUrl>> =
|
||||
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<NormalizedRelayUrl>()
|
||||
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<Set<NormalizedRelayUrl>> =
|
||||
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<NormalizedRelayUrl>()
|
||||
it.forEach {
|
||||
dmRelays.addAll(it)
|
||||
}
|
||||
dmRelays.toSet()
|
||||
},
|
||||
)
|
||||
}.onEach {
|
||||
torEvaluatorFlow.trustedRelays.tryEmit(it)
|
||||
}.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
}
|
||||
@@ -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<Set<NormalizedRelayUrl>>(emptySet())
|
||||
val trustedRelays = MutableStateFlow<Set<NormalizedRelayUrl>>(emptySet())
|
||||
|
||||
val torSettings =
|
||||
combine(
|
||||
settings.torSettings.torType,
|
||||
settings.torSettings.onionRelaysViaTor,
|
||||
settings.torSettings.dmRelaysViaTor,
|
||||
settings.torSettings.trustedRelaysViaTor,
|
||||
settings.torSettings.newRelaysViaTor,
|
||||
torSettingsFlow.torType,
|
||||
torSettingsFlow.onionRelaysViaTor,
|
||||
torSettingsFlow.dmRelaysViaTor,
|
||||
torSettingsFlow.trustedRelaysViaTor,
|
||||
torSettingsFlow.newRelaysViaTor,
|
||||
) {
|
||||
torType: TorType,
|
||||
onionRelaysViaTor: Boolean,
|
||||
@@ -64,11 +67,11 @@ class TorRelayState(
|
||||
}.onStart {
|
||||
emit(
|
||||
TorRelaySettings(
|
||||
torType = settings.torSettings.torType.value,
|
||||
onionRelaysViaTor = settings.torSettings.onionRelaysViaTor.value,
|
||||
dmRelaysViaTor = settings.torSettings.dmRelaysViaTor.value,
|
||||
trustedRelaysViaTor = settings.torSettings.trustedRelaysViaTor.value,
|
||||
newRelaysViaTor = settings.torSettings.newRelaysViaTor.value,
|
||||
torType = torSettingsFlow.torType.value,
|
||||
onionRelaysViaTor = torSettingsFlow.onionRelaysViaTor.value,
|
||||
dmRelaysViaTor = torSettingsFlow.dmRelaysViaTor.value,
|
||||
trustedRelaysViaTor = torSettingsFlow.trustedRelaysViaTor.value,
|
||||
newRelaysViaTor = torSettingsFlow.newRelaysViaTor.value,
|
||||
),
|
||||
)
|
||||
}.flowOn(Dispatchers.Default)
|
||||
@@ -76,20 +79,21 @@ class TorRelayState(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
TorRelaySettings(
|
||||
torType = settings.torSettings.torType.value,
|
||||
onionRelaysViaTor = settings.torSettings.onionRelaysViaTor.value,
|
||||
dmRelaysViaTor = settings.torSettings.dmRelaysViaTor.value,
|
||||
trustedRelaysViaTor = settings.torSettings.trustedRelaysViaTor.value,
|
||||
newRelaysViaTor = settings.torSettings.newRelaysViaTor.value,
|
||||
torType = torSettingsFlow.torType.value,
|
||||
onionRelaysViaTor = torSettingsFlow.onionRelaysViaTor.value,
|
||||
dmRelaysViaTor = torSettingsFlow.dmRelaysViaTor.value,
|
||||
trustedRelaysViaTor = torSettingsFlow.trustedRelaysViaTor.value,
|
||||
newRelaysViaTor = torSettingsFlow.newRelaysViaTor.value,
|
||||
),
|
||||
)
|
||||
|
||||
val flow =
|
||||
combineTransform(
|
||||
torSettings,
|
||||
trustedRelayState.flow,
|
||||
dmRelayState.flow,
|
||||
trustedRelays,
|
||||
dmRelays,
|
||||
) { torSettings: TorRelaySettings, trustedRelayList: Set<NormalizedRelayUrl>, dmRelayList: Set<NormalizedRelayUrl> ->
|
||||
println("AABBCC New tor evaluation: $torSettings $trustedRelayList $dmRelayList")
|
||||
emit(
|
||||
TorRelayEvaluation(
|
||||
torSettings = torSettings,
|
||||
@@ -101,8 +105,8 @@ class TorRelayState(
|
||||
emit(
|
||||
TorRelayEvaluation(
|
||||
torSettings = torSettings.value,
|
||||
trustedRelayList = trustedRelayState.flow.value,
|
||||
dmRelayList = dmRelayState.flow.value,
|
||||
trustedRelayList = trustedRelays.value,
|
||||
dmRelayList = dmRelays.value,
|
||||
),
|
||||
)
|
||||
}.flowOn(Dispatchers.Default)
|
||||
@@ -111,10 +115,12 @@ class TorRelayState(
|
||||
SharingStarted.Eagerly,
|
||||
TorRelayEvaluation(
|
||||
torSettings = torSettings.value,
|
||||
trustedRelayList = trustedRelayState.flow.value,
|
||||
dmRelayList = dmRelayState.flow.value,
|
||||
trustedRelayList = trustedRelays.value,
|
||||
dmRelayList = dmRelays.value,
|
||||
),
|
||||
)
|
||||
|
||||
fun shouldUseTorForClean(relay: NormalizedRelayUrl) = flow.value.useTor(relay)
|
||||
fun shouldUseTorForRelay(relay: NormalizedRelayUrl) = flow.value.useTor(relay)
|
||||
|
||||
fun okHttpClientForRelay(url: NormalizedRelayUrl): OkHttpClient = okHttpClient.getHttpClient(shouldUseTorForRelay(url))
|
||||
}
|
||||
|
||||
+2
-2
@@ -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() }
|
||||
}
|
||||
+1
-1
@@ -39,7 +39,7 @@ class ConnectivityFlow(
|
||||
@OptIn(FlowPreview::class)
|
||||
val status =
|
||||
callbackFlow {
|
||||
trySend(ConnectivityStatus.Connecting)
|
||||
trySend(ConnectivityStatus.StartingService)
|
||||
|
||||
val connectivityManager = context.getConnectivityManager()
|
||||
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ class ConnectivityManager(
|
||||
ConnectivityFlow(context).status.distinctUntilChanged().stateIn(
|
||||
scope,
|
||||
SharingStarted.WhileSubscribed(30000),
|
||||
ConnectivityStatus.Off,
|
||||
ConnectivityStatus.StartingService,
|
||||
)
|
||||
|
||||
val isMobileOrNull: StateFlow<Boolean?> =
|
||||
|
||||
+1
-1
@@ -28,5 +28,5 @@ sealed class ConnectivityStatus {
|
||||
|
||||
object Off : ConnectivityStatus()
|
||||
|
||||
object Connecting : ConnectivityStatus()
|
||||
object StartingService : ConnectivityStatus()
|
||||
}
|
||||
|
||||
+17
-10
@@ -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)
|
||||
}
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ fun VideoViewInner(
|
||||
callbackUri = nostrUriCallback,
|
||||
mimeType = mimeType,
|
||||
aspectRatio = aspectRatio,
|
||||
proxyPort = accountViewModel.proxyPortForVideo(videoUri),
|
||||
proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(videoUri),
|
||||
keepPlaying = true,
|
||||
waveformData = waveform,
|
||||
) { mediaItem ->
|
||||
|
||||
+6
-5
@@ -25,7 +25,6 @@ import com.vitorpamplona.amethyst.model.torState.TorRelayEvaluation
|
||||
import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager
|
||||
import com.vitorpamplona.amethyst.service.connectivity.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager
|
||||
import com.vitorpamplona.amethyst.service.okhttp.ProxySettingsAnchor
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorManager
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorServiceStatus
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
@@ -45,7 +44,7 @@ import net.freehaven.tor.control.TorControlCommands
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class RelayProxyClientConnector(
|
||||
val torProxySettingsAnchor: ProxySettingsAnchor,
|
||||
val torEvaluator: StateFlow<TorRelayEvaluation>,
|
||||
val okHttpClients: DualHttpClientManager,
|
||||
val connManager: ConnectivityManager,
|
||||
val client: INostrClient,
|
||||
@@ -53,7 +52,7 @@ class RelayProxyClientConnector(
|
||||
val scope: CoroutineScope,
|
||||
) {
|
||||
data class RelayServiceInfra(
|
||||
val torSettings: StateFlow<TorRelayEvaluation>,
|
||||
val evaluator: TorRelayEvaluation,
|
||||
val torConnection: OkHttpClient,
|
||||
val clearConnection: OkHttpClient,
|
||||
val connectivity: ConnectivityStatus,
|
||||
@@ -62,7 +61,7 @@ class RelayProxyClientConnector(
|
||||
@OptIn(FlowPreview::class)
|
||||
val relayServices =
|
||||
combine(
|
||||
torProxySettingsAnchor.flow,
|
||||
torEvaluator,
|
||||
okHttpClients.defaultHttpClient,
|
||||
okHttpClients.defaultHttpClientWithoutProxy,
|
||||
connManager.status,
|
||||
@@ -70,7 +69,9 @@ class RelayProxyClientConnector(
|
||||
RelayServiceInfra(torSettings, torConnection, clearConnection, connectivity)
|
||||
}.debounce(100)
|
||||
.onEach {
|
||||
if (it.connectivity is ConnectivityStatus.Off) {
|
||||
if (it.connectivity is ConnectivityStatus.StartingService) {
|
||||
// ignore
|
||||
} else if (it.connectivity is ConnectivityStatus.Off) {
|
||||
Log.d("ManageRelayServices", "Pausing Relay Services ${it.connectivity}")
|
||||
if (client.isActive()) {
|
||||
client.disconnect()
|
||||
|
||||
+4
-4
@@ -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,
|
||||
)
|
||||
|
||||
@@ -28,6 +28,7 @@ import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.debugState
|
||||
@@ -40,7 +41,6 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.prepareSharedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||
@@ -67,16 +67,14 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
setContent {
|
||||
StringResSetup()
|
||||
|
||||
val sharedPreferencesViewModel = prepareSharedViewModel()
|
||||
AmethystTheme(sharedPreferencesViewModel.sharedPrefs.theme) {
|
||||
AmethystTheme {
|
||||
val accountStateViewModel: AccountStateViewModel = viewModel()
|
||||
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
accountStateViewModel.loginWithDefaultAccountIfLoggedOff()
|
||||
}
|
||||
|
||||
AccountScreen(accountStateViewModel, sharedPreferencesViewModel)
|
||||
AccountScreen(accountStateViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <T> CrossfadeIfEnabled(
|
||||
accountViewModel: AccountViewModel,
|
||||
content: @Composable (T) -> Unit,
|
||||
) {
|
||||
if (accountViewModel.settings.featureSet == FeatureSetType.PERFORMANCE) {
|
||||
if (accountViewModel.settings.isPerformanceMode()) {
|
||||
Box(modifier, contentAlignment) {
|
||||
content(targetState)
|
||||
}
|
||||
|
||||
+2
-2
@@ -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,
|
||||
)
|
||||
|
||||
@@ -56,6 +56,6 @@ tailrec fun Context.getActivity(): ComponentActivity =
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getAmethystApp(): Amethyst = LocalContext.current.getAmethystApp()
|
||||
fun amethystApp(): Amethyst = LocalContext.current.asAmethystApp()
|
||||
|
||||
fun Context.getAmethystApp(): Amethyst = this.applicationContext as Amethyst
|
||||
fun Context.asAmethystApp(): Amethyst = this.applicationContext as Amethyst
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-3
@@ -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)
|
||||
|
||||
@@ -39,6 +39,7 @@ import androidx.core.net.toUri
|
||||
import androidx.core.util.Consumer
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.crashreports.DisplayCrashMessages
|
||||
import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.compose.DisplayNotifyMessages
|
||||
@@ -54,7 +55,6 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.isBaseRoute
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.isSameRoute
|
||||
import com.vitorpamplona.amethyst.ui.note.nip22Comments.ReplyCommentPostScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.BookmarkListScreen
|
||||
@@ -106,7 +106,6 @@ import java.net.URI
|
||||
fun AppNavigation(
|
||||
accountViewModel: AccountViewModel,
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
sharedPreferencesViewModel: SharedPreferencesViewModel,
|
||||
) {
|
||||
val nav = rememberNav()
|
||||
|
||||
@@ -121,16 +120,16 @@ fun AppNavigation(
|
||||
composable<Route.Message> { MessagesScreen(accountViewModel, nav) }
|
||||
composable<Route.Video> { VideoScreen(accountViewModel, nav) }
|
||||
composable<Route.Discover> { DiscoverScreen(accountViewModel, nav) }
|
||||
composable<Route.Notification> { NotificationScreen(sharedPreferencesViewModel, accountViewModel, nav) }
|
||||
composable<Route.Notification> { NotificationScreen(accountViewModel, nav) }
|
||||
|
||||
composable<Route.EditProfile> { NewUserMetadataScreen(nav, accountViewModel) }
|
||||
composable<Route.Search> { SearchScreen(accountViewModel, nav) }
|
||||
|
||||
composableFromEnd<Route.SecurityFilters> { SecurityFiltersScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.PrivacyOptions> { PrivacyOptionsScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.PrivacyOptions> { PrivacyOptionsScreen(Amethyst.instance.torPrefs.value, nav) }
|
||||
composableFromEnd<Route.Bookmarks> { BookmarkListScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.Drafts> { DraftListScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.Settings> { SettingsScreen(sharedPreferencesViewModel, accountViewModel, nav) }
|
||||
composableFromEnd<Route.Settings> { SettingsScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.UserSettings> { UserSettingsScreen(accountViewModel, nav) }
|
||||
composableFromBottomArgs<Route.Nip47NWCSetup> { NIP47SetupScreen(accountViewModel, nav, it.nip47) }
|
||||
composableFromEndArgs<Route.EditRelays> { AllRelayListScreen(accountViewModel, nav) }
|
||||
|
||||
+1
-2
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -91,7 +91,6 @@ import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollowerCount
|
||||
@@ -247,7 +246,7 @@ fun ProfileContentTemplate(
|
||||
.border(3.dp, MaterialTheme.colorScheme.onBackground, CircleShape)
|
||||
.clickable(onClick = onClick),
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
)
|
||||
|
||||
if (bestDisplayName != null) {
|
||||
|
||||
+1
-2
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Int>(
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+1
-4
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ fun VoiceHeader(
|
||||
callbackUri = callbackUri,
|
||||
mimeType = null,
|
||||
aspectRatio = null,
|
||||
proxyPort = accountViewModel.proxyPortForVideo(media),
|
||||
proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(media),
|
||||
keepPlaying = false,
|
||||
waveformData = waveform,
|
||||
) { mediaItem ->
|
||||
|
||||
@@ -34,16 +34,17 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoggedInPage
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginOrSignupScreen
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
@Composable
|
||||
fun AccountScreen(
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
sharedPreferencesViewModel: SharedPreferencesViewModel,
|
||||
) {
|
||||
fun AccountScreen(accountStateViewModel: AccountStateViewModel) {
|
||||
// Pauses relay services when the app pauses
|
||||
ManageRelayServices()
|
||||
|
||||
val accountState by accountStateViewModel.accountContent.collectAsStateWithLifecycle()
|
||||
|
||||
Log.d("ActivityLifecycle", "AccountScreen $accountState $accountStateViewModel")
|
||||
@@ -55,11 +56,17 @@ fun AccountScreen(
|
||||
when (state) {
|
||||
is AccountState.Loading -> LoadingSetup()
|
||||
is AccountState.LoggedOff -> LoggedOffSetup(accountStateViewModel)
|
||||
is AccountState.LoggedIn -> LoggedInSetup(state, accountStateViewModel, sharedPreferencesViewModel)
|
||||
is AccountState.LoggedIn -> LoggedInSetup(state, accountStateViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ManageRelayServices() {
|
||||
val relayServices by Amethyst.instance.relayProxyClientConnector.relayServices
|
||||
.collectAsStateWithLifecycle()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadingSetup() {
|
||||
// A surface container using the 'background' color from the theme
|
||||
@@ -91,14 +98,12 @@ fun LoggedOffSetup(accountStateViewModel: AccountStateViewModel) {
|
||||
fun LoggedInSetup(
|
||||
state: AccountState.LoggedIn,
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
sharedPreferencesViewModel: SharedPreferencesViewModel,
|
||||
) {
|
||||
SetAccountCentricViewModelStore(state) {
|
||||
LoggedInPage(
|
||||
state.account,
|
||||
state.route,
|
||||
accountStateViewModel,
|
||||
sharedPreferencesViewModel,
|
||||
account = state.account,
|
||||
route = state.route,
|
||||
accountStateViewModel = accountStateViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+8
-26
@@ -35,8 +35,6 @@ import com.vitorpamplona.amethyst.model.DefaultNIP65RelaySet
|
||||
import com.vitorpamplona.amethyst.model.DefaultSearchRelayList
|
||||
import com.vitorpamplona.amethyst.service.Nip05NostrAddressVerifier
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
@@ -108,7 +106,6 @@ class AccountStateViewModel : ViewModel() {
|
||||
|
||||
suspend fun loginAndStartUI(
|
||||
key: String,
|
||||
torSettings: TorSettings,
|
||||
transientAccount: Boolean,
|
||||
loginWithExternalSigner: Boolean = false,
|
||||
packageName: String = "",
|
||||
@@ -142,31 +139,26 @@ class AccountStateViewModel : ViewModel() {
|
||||
keyPair = KeyPair(pubKey = pubKeyParsed),
|
||||
transientAccount = transientAccount,
|
||||
externalSignerPackageName = packageName.ifBlank { "com.greenart7c3.nostrsigner" },
|
||||
torSettings = TorSettingsFlow.build(torSettings),
|
||||
)
|
||||
} else if (key.startsWith("nsec")) {
|
||||
AccountSettings(
|
||||
keyPair = KeyPair(privKey = key.bechToBytes()),
|
||||
transientAccount = transientAccount,
|
||||
torSettings = TorSettingsFlow.build(torSettings),
|
||||
)
|
||||
} else if (key.contains(" ") && Nip06().isValidMnemonic(key)) {
|
||||
AccountSettings(
|
||||
keyPair = KeyPair(privKey = Nip06().privateKeyFromMnemonic(key)),
|
||||
transientAccount = transientAccount,
|
||||
torSettings = TorSettingsFlow.build(torSettings),
|
||||
)
|
||||
} else if (pubKeyParsed != null) {
|
||||
AccountSettings(
|
||||
keyPair = KeyPair(pubKey = pubKeyParsed),
|
||||
transientAccount = transientAccount,
|
||||
torSettings = TorSettingsFlow.build(torSettings),
|
||||
)
|
||||
} else {
|
||||
AccountSettings(
|
||||
keyPair = KeyPair(Hex.decode(key)),
|
||||
transientAccount = transientAccount,
|
||||
torSettings = TorSettingsFlow.build(torSettings),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -197,7 +189,6 @@ class AccountStateViewModel : ViewModel() {
|
||||
fun login(
|
||||
key: String,
|
||||
password: String,
|
||||
torSettings: TorSettings,
|
||||
transientAccount: Boolean,
|
||||
loginWithExternalSigner: Boolean = false,
|
||||
packageName: String = "",
|
||||
@@ -222,41 +213,39 @@ class AccountStateViewModel : ViewModel() {
|
||||
onError("Could not decrypt key with provided password")
|
||||
Log.e("Login", "Could not decrypt ncryptsec")
|
||||
} else {
|
||||
loginSync(newKey, torSettings, transientAccount, loginWithExternalSigner, packageName, onError)
|
||||
loginSync(newKey, transientAccount, loginWithExternalSigner, packageName, onError)
|
||||
}
|
||||
} else if (EMAIL_PATTERN.matcher(key).matches()) {
|
||||
Nip05NostrAddressVerifier().verifyNip05(
|
||||
key,
|
||||
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(false) },
|
||||
onSuccess = { publicKey ->
|
||||
loginSync(Hex.decode(publicKey).toNpub(), torSettings, transientAccount, loginWithExternalSigner, packageName, onError)
|
||||
loginSync(Hex.decode(publicKey).toNpub(), transientAccount, loginWithExternalSigner, packageName, onError)
|
||||
},
|
||||
onError = {
|
||||
onError(it)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
loginSync(key, torSettings, transientAccount, loginWithExternalSigner, packageName, onError)
|
||||
loginSync(key, transientAccount, loginWithExternalSigner, packageName, onError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun login(
|
||||
key: String,
|
||||
torSettings: TorSettings,
|
||||
transientAccount: Boolean,
|
||||
loginWithExternalSigner: Boolean = false,
|
||||
packageName: String = "",
|
||||
onError: (String) -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
loginSync(key, torSettings, transientAccount, loginWithExternalSigner, packageName, onError)
|
||||
loginSync(key, transientAccount, loginWithExternalSigner, packageName, onError)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loginSync(
|
||||
key: String,
|
||||
torSettings: TorSettings,
|
||||
transientAccount: Boolean,
|
||||
loginWithExternalSigner: Boolean = false,
|
||||
packageName: String = "",
|
||||
@@ -266,7 +255,7 @@ class AccountStateViewModel : ViewModel() {
|
||||
if (_accountContent.value is AccountState.LoggedIn) {
|
||||
prepareLogoutOrSwitch()
|
||||
}
|
||||
loginAndStartUI(key, torSettings, transientAccount, loginWithExternalSigner, packageName)
|
||||
loginAndStartUI(key, transientAccount, loginWithExternalSigner, packageName)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("Login", "Could not sign in", e)
|
||||
@@ -274,10 +263,7 @@ class AccountStateViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun newKey(
|
||||
torSettings: TorSettings,
|
||||
name: String? = null,
|
||||
) {
|
||||
fun newKey(name: String? = null) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (_accountContent.value is AccountState.LoggedIn) {
|
||||
prepareLogoutOrSwitch()
|
||||
@@ -285,7 +271,7 @@ class AccountStateViewModel : ViewModel() {
|
||||
|
||||
_accountContent.update { AccountState.Loading }
|
||||
|
||||
val accountSettings = createNewAccount(torSettings, name)
|
||||
val accountSettings = createNewAccount(name)
|
||||
|
||||
LocalPreferences.setDefaultAccount(accountSettings)
|
||||
|
||||
@@ -306,10 +292,7 @@ class AccountStateViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun createNewAccount(
|
||||
torSettings: TorSettings,
|
||||
name: String? = null,
|
||||
): AccountSettings {
|
||||
fun createNewAccount(name: String? = null): AccountSettings {
|
||||
val keyPair = KeyPair()
|
||||
val tempSigner = NostrSignerSync(keyPair)
|
||||
|
||||
@@ -327,7 +310,6 @@ class AccountStateViewModel : ViewModel() {
|
||||
backupDMRelayList = ChatMessageRelayListEvent.create(DefaultDMRelayList, tempSigner),
|
||||
backupSearchRelayList = SearchRelayListEvent.create(DefaultSearchRelayList.toList(), tempSigner),
|
||||
backupChannelList = ChannelListEvent.create(emptyList(), DefaultChannels, tempSigner),
|
||||
torSettings = TorSettingsFlow.build(torSettings),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
-72
@@ -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)
|
||||
}
|
||||
}
|
||||
-211
@@ -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<DisplayFeature>,
|
||||
) {
|
||||
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
|
||||
}
|
||||
@@ -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<String?>(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<Boolean>(false)
|
||||
var dontAskForNotificationPermissions by mutableStateOf<Boolean>(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<WindowSizeClass?>(null)
|
||||
var displayFeatures = mutableStateOf<List<DisplayFeature>>(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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Boolean>,
|
||||
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
|
||||
}
|
||||
+52
-71
@@ -33,7 +33,6 @@ import androidx.core.net.toUri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil3.asDrawable
|
||||
import coil3.imageLoader
|
||||
import coil3.request.ImageRequest
|
||||
@@ -49,6 +48,7 @@ import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
||||
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
|
||||
@@ -56,9 +56,10 @@ import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
|
||||
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.observables.CreatedAtComparator
|
||||
import com.vitorpamplona.amethyst.model.privacyOptions.EmptyRoleBasedHttpClientBuilder
|
||||
import com.vitorpamplona.amethyst.model.privacyOptions.IRoleBasedHttpClientBuilder
|
||||
import com.vitorpamplona.amethyst.model.privacyOptions.RoleBasedHttpClientBuilder
|
||||
import com.vitorpamplona.amethyst.service.Nip05NostrAddressVerifier
|
||||
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
|
||||
import com.vitorpamplona.amethyst.service.Nip11Retriever
|
||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||
import com.vitorpamplona.amethyst.service.cashu.CashuToken
|
||||
@@ -66,8 +67,6 @@ import com.vitorpamplona.amethyst.service.cashu.melt.MeltProcessor
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.service.okhttp.EmptyHttpClientManager
|
||||
import com.vitorpamplona.amethyst.service.okhttp.IHttpClientManager
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
|
||||
import com.vitorpamplona.amethyst.service.uploads.CompressorQuality
|
||||
@@ -84,12 +83,12 @@ import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmountInteger
|
||||
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.SharedSettingsState
|
||||
import com.vitorpamplona.amethyst.ui.screen.UiSettingsState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CombinedZap
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorType
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent
|
||||
@@ -106,8 +105,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
|
||||
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.DefaultOtsResolverBuilder
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
@@ -160,15 +158,15 @@ import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import java.util.Locale
|
||||
|
||||
@Stable
|
||||
class AccountViewModel(
|
||||
val account: Account,
|
||||
val settings: SharedSettingsState,
|
||||
val settings: UiSettingsState,
|
||||
val torSettings: TorSettingsFlow,
|
||||
val dataSources: RelaySubscriptionsCoordinator,
|
||||
val okHttpClient: IHttpClientManager,
|
||||
val httpClientBuilder: IRoleBasedHttpClientBuilder,
|
||||
) : ViewModel(),
|
||||
Dao {
|
||||
var firstRoute: Route? = null
|
||||
@@ -650,7 +648,7 @@ class AccountViewModel(
|
||||
message = message,
|
||||
context = context,
|
||||
showErrorIfNoLnAddress = showErrorIfNoLnAddress,
|
||||
okHttpClient = ::okHttpClientForMoney,
|
||||
okHttpClient = httpClientBuilder::okHttpClientForMoney,
|
||||
onError = onError,
|
||||
onProgress = onProgress,
|
||||
onPayViaIntent = onPayViaIntent,
|
||||
@@ -886,7 +884,7 @@ class AccountViewModel(
|
||||
onResult: suspend (UrlPreviewState) -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
UrlCachedPreviewer.previewInfo(url, ::okHttpClientForPreview, onResult)
|
||||
UrlCachedPreviewer.previewInfo(url, httpClientBuilder::okHttpClientForPreview, onResult)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -907,9 +905,7 @@ class AccountViewModel(
|
||||
Nip05NostrAddressVerifier()
|
||||
.verifyNip05(
|
||||
nip05,
|
||||
okHttpClient = {
|
||||
okHttpClient.getHttpClient(account.privacyState.shouldUseTorForNIP05(it))
|
||||
},
|
||||
okHttpClient = httpClientBuilder::okHttpClientForNip05,
|
||||
onSuccess = {
|
||||
// Marks user as verified
|
||||
if (it == pubkeyHex) {
|
||||
@@ -936,21 +932,6 @@ class AccountViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun retrieveRelayDocument(
|
||||
relay: NormalizedRelayUrl,
|
||||
onInfo: (Nip11RelayInformation) -> Unit,
|
||||
onError: (NormalizedRelayUrl, Nip11Retriever.ErrorCode, String?) -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
Nip11CachedRetriever.loadRelayInfo(
|
||||
relay,
|
||||
okHttpClient = { okHttpClientForClean(relay) },
|
||||
onInfo,
|
||||
onError,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun runOnIO(runOnIO: () -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) { runOnIO() }
|
||||
}
|
||||
@@ -1073,7 +1054,7 @@ class AccountViewModel(
|
||||
|
||||
suspend fun checkVideoIsOnline(videoUrl: String): Boolean =
|
||||
withContext(Dispatchers.IO) {
|
||||
OnlineChecker.isOnline(videoUrl, ::okHttpClientForVideo)
|
||||
OnlineChecker.isOnline(videoUrl, httpClientBuilder::okHttpClientForVideo)
|
||||
}
|
||||
|
||||
fun loadAndMarkAsRead(
|
||||
@@ -1108,19 +1089,19 @@ class AccountViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun setTorSettings(newTorSettings: TorSettings) = runIOCatching { account.settings.setTorSettings(newTorSettings) }
|
||||
|
||||
class Factory(
|
||||
val account: Account,
|
||||
val settings: SharedSettingsState,
|
||||
val settings: UiSettingsState,
|
||||
val torSettings: TorSettingsFlow,
|
||||
val dataSources: RelaySubscriptionsCoordinator,
|
||||
val okHttpClient: IHttpClientManager,
|
||||
val okHttpClient: RoleBasedHttpClientBuilder,
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
||||
AccountViewModel(
|
||||
account,
|
||||
settings,
|
||||
torSettings,
|
||||
dataSources,
|
||||
okHttpClient,
|
||||
) as T
|
||||
@@ -1278,7 +1259,7 @@ class AccountViewModel(
|
||||
if (lud16 != null) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val meltResult = MeltProcessor().melt(token, lud16, ::okHttpClientForMoney, context)
|
||||
val meltResult = MeltProcessor().melt(token, lud16, httpClientBuilder::okHttpClientForMoney, context)
|
||||
onDone(
|
||||
stringRes(context, R.string.cashu_successful_redemption),
|
||||
stringRes(
|
||||
@@ -1395,22 +1376,6 @@ class AccountViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun proxyPortForVideo(url: String): Int? = okHttpClient.getCurrentProxyPort(account.privacyState.shouldUseTorForVideoDownload(url))
|
||||
|
||||
fun okHttpClientForNip96(url: String): OkHttpClient = okHttpClient.getHttpClient(account.privacyState.shouldUseTorForUploads(url))
|
||||
|
||||
fun okHttpClientForImage(url: String): OkHttpClient = okHttpClient.getHttpClient(account.privacyState.shouldUseTorForImageDownload(url))
|
||||
|
||||
fun okHttpClientForVideo(url: String): OkHttpClient = okHttpClient.getHttpClient(account.privacyState.shouldUseTorForVideoDownload(url))
|
||||
|
||||
fun okHttpClientForMoney(url: String): OkHttpClient = okHttpClient.getHttpClient(account.privacyState.shouldUseTorForMoneyOperations(url))
|
||||
|
||||
fun okHttpClientForPreview(url: String): OkHttpClient = okHttpClient.getHttpClient(account.privacyState.shouldUseTorForPreviewUrl(url))
|
||||
|
||||
fun okHttpClientForClean(url: NormalizedRelayUrl): OkHttpClient = okHttpClient.getHttpClient(account.torRelayState.shouldUseTorForClean(url))
|
||||
|
||||
fun okHttpClientForPushRegistration(url: String): OkHttpClient = okHttpClient.getHttpClient(account.privacyState.shouldUseTorForTrustedRelays())
|
||||
|
||||
fun dataSources() = dataSources
|
||||
|
||||
suspend fun createTempDraftNote(noteEvent: DraftWrapEvent): Note? = draftNoteCache.update(noteEvent)
|
||||
@@ -1526,7 +1491,7 @@ class AccountViewModel(
|
||||
milliSats = milliSats,
|
||||
message = message,
|
||||
nostrRequest = zapRequest,
|
||||
okHttpClient = ::okHttpClientForMoney,
|
||||
okHttpClient = httpClientBuilder::okHttpClientForMoney,
|
||||
onProgress = onProgress,
|
||||
context = context,
|
||||
)
|
||||
@@ -1549,7 +1514,7 @@ class AccountViewModel(
|
||||
viewModelScope.launch {
|
||||
MediaSaverToDisk.saveDownloadingIfNeeded(
|
||||
videoUri = videoUri,
|
||||
okHttpClient = ::okHttpClientForVideo,
|
||||
okHttpClient = httpClientBuilder::okHttpClientForVideo,
|
||||
mimeType = mimeType,
|
||||
localContext = localContext,
|
||||
onSuccess = {
|
||||
@@ -1668,8 +1633,17 @@ var mockedCache: AccountViewModel? = null
|
||||
fun mockAccountViewModel(): AccountViewModel {
|
||||
mockedCache?.let { return it }
|
||||
|
||||
val sharedPreferencesViewModel: SharedPreferencesViewModel = viewModel()
|
||||
sharedPreferencesViewModel.init()
|
||||
val otsResolver = DefaultOtsResolverBuilder()
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val uiState =
|
||||
UiSettingsState(
|
||||
uiSettingsFlow = UiSettingsFlow(),
|
||||
isMobileOrMeteredConnection = MutableStateFlow(false),
|
||||
scope = scope,
|
||||
)
|
||||
|
||||
val keyPair =
|
||||
KeyPair(
|
||||
privKey = Hex.decode("0f761f8a5a481e26f06605a1d9b3e9eba7a107d351f43c43a57469b788274499"),
|
||||
@@ -1677,8 +1651,6 @@ fun mockAccountViewModel(): AccountViewModel {
|
||||
forceReplacePubkey = false,
|
||||
)
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val client = EmptyNostrClient
|
||||
|
||||
val nwcFilters = NWCPaymentFilterAssembler(client)
|
||||
@@ -1689,7 +1661,7 @@ fun mockAccountViewModel(): AccountViewModel {
|
||||
signer = NostrSignerInternal(keyPair),
|
||||
geolocationFlow = MutableStateFlow<LocationState.LocationResult>(LocationState.LocationResult.Loading),
|
||||
nwcFilterAssembler = nwcFilters,
|
||||
otsVerifCache = VerificationStateCache(),
|
||||
otsResolverBuilder = otsResolver,
|
||||
cache = LocalCache,
|
||||
client = client,
|
||||
scope = scope,
|
||||
@@ -1697,8 +1669,9 @@ fun mockAccountViewModel(): AccountViewModel {
|
||||
|
||||
return AccountViewModel(
|
||||
account = account,
|
||||
settings = sharedPreferencesViewModel.sharedPrefs,
|
||||
okHttpClient = EmptyHttpClientManager,
|
||||
settings = uiState,
|
||||
torSettings = TorSettingsFlow(torType = MutableStateFlow(TorType.OFF)),
|
||||
httpClientBuilder = EmptyRoleBasedHttpClientBuilder(),
|
||||
dataSources = RelaySubscriptionsCoordinator(LocalCache, client, scope),
|
||||
).also {
|
||||
mockedCache = it
|
||||
@@ -1712,8 +1685,17 @@ var vitorCache: AccountViewModel? = null
|
||||
fun mockVitorAccountViewModel(): AccountViewModel {
|
||||
mockedCache?.let { return it }
|
||||
|
||||
val sharedPreferencesViewModel: SharedPreferencesViewModel = viewModel()
|
||||
sharedPreferencesViewModel.init()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val otsResolver = DefaultOtsResolverBuilder()
|
||||
|
||||
val uiState =
|
||||
UiSettingsState(
|
||||
uiSettingsFlow = UiSettingsFlow(),
|
||||
isMobileOrMeteredConnection = MutableStateFlow(false),
|
||||
scope = scope,
|
||||
)
|
||||
|
||||
val keyPair =
|
||||
KeyPair(
|
||||
pubKey = Hex.decode("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
||||
@@ -1723,15 +1705,13 @@ fun mockVitorAccountViewModel(): AccountViewModel {
|
||||
|
||||
val nwcFilters = NWCPaymentFilterAssembler(client)
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val account =
|
||||
Account(
|
||||
settings = AccountSettings(keyPair),
|
||||
signer = NostrSignerInternal(keyPair),
|
||||
geolocationFlow = MutableStateFlow<LocationState.LocationResult>(LocationState.LocationResult.Loading),
|
||||
nwcFilterAssembler = nwcFilters,
|
||||
otsVerifCache = VerificationStateCache(),
|
||||
otsResolverBuilder = otsResolver,
|
||||
cache = LocalCache,
|
||||
client = EmptyNostrClient,
|
||||
scope = scope,
|
||||
@@ -1739,8 +1719,9 @@ fun mockVitorAccountViewModel(): AccountViewModel {
|
||||
|
||||
return AccountViewModel(
|
||||
account = account,
|
||||
settings = sharedPreferencesViewModel.sharedPrefs,
|
||||
okHttpClient = EmptyHttpClientManager,
|
||||
settings = uiState,
|
||||
torSettings = TorSettingsFlow(torType = MutableStateFlow(TorType.OFF)),
|
||||
httpClientBuilder = EmptyRoleBasedHttpClientBuilder(),
|
||||
dataSources = RelaySubscriptionsCoordinator(LocalCache, client, scope),
|
||||
).also {
|
||||
vitorCache = it
|
||||
|
||||
+5
-38
@@ -30,7 +30,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
@@ -49,7 +48,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.Account
|
||||
import com.vitorpamplona.amethyst.ui.navigation.AppNavigation
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssemblerSubscription
|
||||
@@ -62,7 +60,6 @@ fun LoggedInPage(
|
||||
account: Account,
|
||||
route: Route?,
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
sharedPreferencesViewModel: SharedPreferencesViewModel,
|
||||
) {
|
||||
val accountViewModel: AccountViewModel =
|
||||
viewModel(
|
||||
@@ -70,9 +67,10 @@ fun LoggedInPage(
|
||||
factory =
|
||||
AccountViewModel.Factory(
|
||||
account = account,
|
||||
settings = sharedPreferencesViewModel.sharedPrefs,
|
||||
settings = Amethyst.instance.uiState,
|
||||
torSettings = Amethyst.instance.torPrefs.value,
|
||||
dataSources = Amethyst.instance.sources,
|
||||
okHttpClient = Amethyst.instance.okHttpClients,
|
||||
okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -81,12 +79,6 @@ fun LoggedInPage(
|
||||
// Adds this account to the authentication procedures for relays.
|
||||
RelayAuthSubscription(accountViewModel)
|
||||
|
||||
// Sets up Coil's Image Loader
|
||||
ObserveImageLoadingTor(accountViewModel)
|
||||
|
||||
// Sets up the use of Proxy based on this Account's settings
|
||||
SetProxyDeterminator(accountViewModel)
|
||||
|
||||
// Loads account information + DMs and Notifications from Relays.
|
||||
AccountFilterAssemblerSubscription(accountViewModel)
|
||||
|
||||
@@ -99,9 +91,6 @@ fun LoggedInPage(
|
||||
// Updates local cache of the anti-spam filter choice of this user.
|
||||
ObserveAntiSpamFilterSettings(accountViewModel)
|
||||
|
||||
// Pauses relay services when the app pauses
|
||||
ManageRelayServices(accountViewModel)
|
||||
|
||||
// Listens to Amber
|
||||
ListenToExternalSignerIfNeeded(accountViewModel)
|
||||
|
||||
@@ -111,7 +100,6 @@ fun LoggedInPage(
|
||||
AppNavigation(
|
||||
accountViewModel = accountViewModel,
|
||||
accountStateViewModel = accountStateViewModel,
|
||||
sharedPreferencesViewModel = sharedPreferencesViewModel,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -123,27 +111,6 @@ fun ObserveAntiSpamFilterSettings(accountViewModel: AccountViewModel) {
|
||||
Amethyst.instance.cache.antiSpam.active = isSpamActive
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SetProxyDeterminator(accountViewModel: AccountViewModel) {
|
||||
LaunchedEffect(accountViewModel) {
|
||||
Amethyst.instance.torProxySettingsAnchor.flow
|
||||
.tryEmit(accountViewModel.account.torRelayState.flow)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ObserveImageLoadingTor(accountViewModel: AccountViewModel) {
|
||||
LaunchedEffect(accountViewModel) {
|
||||
Amethyst.instance.setImageLoader(accountViewModel.account.privacyState::shouldUseTorForImageDownload)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ManageRelayServices(accountViewModel: AccountViewModel) {
|
||||
val relayServices by Amethyst.instance.relayProxyClientConnector.relayServices
|
||||
.collectAsStateWithLifecycle()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun NotificationRegistration(accountViewModel: AccountViewModel) {
|
||||
@@ -158,7 +125,7 @@ fun NotificationRegistration(accountViewModel: AccountViewModel) {
|
||||
scope.launch {
|
||||
PushNotificationUtils.checkAndInit(
|
||||
LocalPreferences.allSavedAccounts(),
|
||||
accountViewModel::okHttpClientForPushRegistration,
|
||||
accountViewModel.httpClientBuilder::okHttpClientForPushRegistration,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -173,7 +140,7 @@ fun NotificationRegistration(accountViewModel: AccountViewModel) {
|
||||
scope.launch {
|
||||
PushNotificationUtils.checkAndInit(
|
||||
LocalPreferences.allSavedAccounts(),
|
||||
accountViewModel::okHttpClientForPushRegistration,
|
||||
accountViewModel.httpClientBuilder::okHttpClientForPushRegistration,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -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,
|
||||
|
||||
+4
-5
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-30
@@ -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<Nip11RelayInformation> =
|
||||
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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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,
|
||||
)
|
||||
|
||||
+1
-2
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+1
-3
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-5
@@ -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)) },
|
||||
)
|
||||
}
|
||||
|
||||
+3
-4
@@ -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)
|
||||
|
||||
+2
-6
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-35
@@ -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,
|
||||
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+25
-10
@@ -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<TorDialogViewModel>()
|
||||
|
||||
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()
|
||||
},
|
||||
)
|
||||
|
||||
+1
-1
@@ -297,7 +297,7 @@ fun UrlVideoView(
|
||||
callbackUri = content.uri,
|
||||
mimeType = content.mimeType,
|
||||
aspectRatio = ratio,
|
||||
proxyPort = accountViewModel.proxyPortForVideo(content.url),
|
||||
proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(content.url),
|
||||
) { mediaItem ->
|
||||
GetVideoController(
|
||||
mediaItem = mediaItem,
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+2
-3
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -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(
|
||||
|
||||
+3
-4
@@ -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,
|
||||
)
|
||||
|
||||
+2
-2
@@ -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(),
|
||||
|
||||
+1
-2
@@ -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)) },
|
||||
|
||||
+2
-6
@@ -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 }
|
||||
|
||||
+2
-1
@@ -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 ->
|
||||
|
||||
+2
-5
@@ -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)
|
||||
},
|
||||
|
||||
+5
-6
@@ -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)) },
|
||||
)
|
||||
|
||||
|
||||
+205
-160
@@ -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<CharSequence>()
|
||||
try {
|
||||
@@ -116,9 +165,8 @@ fun Context.getLangPreferenceDropdownEntries(): ImmutableMap<String, String> {
|
||||
|
||||
fun getLanguageIndex(
|
||||
languageEntries: ImmutableMap<String, String>,
|
||||
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<TitleExplainer>,
|
||||
selectedItems: ImmutableList<TitleExplainer>,
|
||||
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)),
|
||||
)
|
||||
|
||||
+1
-2
@@ -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!!,
|
||||
|
||||
+5
-5
@@ -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,
|
||||
)
|
||||
|
||||
+4
-4
@@ -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(
|
||||
|
||||
-9
@@ -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,
|
||||
|
||||
+15
-15
@@ -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)) {
|
||||
|
||||
+1
-8
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,10 +54,12 @@ import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.halilibo.richtext.ui.RichTextStyle
|
||||
import com.halilibo.richtext.ui.resolveDefaults
|
||||
import com.patrykandpatrick.vico.compose.common.VicoTheme
|
||||
import com.patrykandpatrick.vico.compose.common.VicoTheme.CandlestickCartesianLayerColors
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.model.ThemeType
|
||||
|
||||
private val DarkColorPalette =
|
||||
@@ -482,6 +484,14 @@ val chartDarkColors =
|
||||
val ColorScheme.chartStyle: VicoTheme
|
||||
get() = if (isLight) chartLightColors else chartDarkColors
|
||||
|
||||
@Composable
|
||||
fun AmethystTheme(content: @Composable () -> Unit) {
|
||||
val theme by Amethyst.instance.uiPrefs.value.theme
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
AmethystTheme(theme, content)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AmethystTheme(
|
||||
prefTheme: ThemeType,
|
||||
|
||||
@@ -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<TorServiceStatus> =
|
||||
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<Int?> =
|
||||
status
|
||||
|
||||
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.tor
|
||||
|
||||
import com.vitorpamplona.amethyst.R
|
||||
|
||||
class TorSettings(
|
||||
data class TorSettings(
|
||||
val torType: TorType = TorType.INTERNAL,
|
||||
val externalSocksPort: Int = 9050,
|
||||
val onionRelaysViaTor: Boolean = true,
|
||||
|
||||
@@ -94,6 +94,8 @@ class TorSettingsFlow(
|
||||
fun update(torSettings: TorSettings): Boolean {
|
||||
var any = false
|
||||
|
||||
println("AABBCC updating 1 $torSettings")
|
||||
|
||||
if (torType.value != torSettings.torType) {
|
||||
torType.tryEmit(torSettings.torType)
|
||||
any = true
|
||||
@@ -148,6 +150,8 @@ class TorSettingsFlow(
|
||||
any = true
|
||||
}
|
||||
|
||||
println("AABBCC updating 2 ${toSettings()}")
|
||||
|
||||
return any
|
||||
}
|
||||
|
||||
|
||||
+22
-5
@@ -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) {}
|
||||
|
||||
+1
-1
@@ -459,7 +459,7 @@ open class BasicRelayClient(
|
||||
)
|
||||
}
|
||||
socket?.let {
|
||||
// Log.d(logTag, "Sending: $str")
|
||||
Log.d(logTag, "Sending: $str")
|
||||
val result = it.send(str)
|
||||
listener.onSend(this@BasicRelayClient, str, result)
|
||||
stats.addBytesSent(str.bytesUsedInMemory())
|
||||
|
||||
+9
-13
@@ -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<HexKey, VerificationState>(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user