- 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
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
@@ -36,6 +38,7 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -44,8 +47,10 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
|
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
|
||||||
import com.halilibo.richtext.commonmark.MarkdownParseOptions
|
import com.halilibo.richtext.commonmark.MarkdownParseOptions
|
||||||
import com.halilibo.richtext.markdown.BasicMarkdown
|
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.material3.RichText
|
||||||
import com.halilibo.richtext.ui.resolveDefaults
|
import com.halilibo.richtext.ui.resolveDefaults
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
||||||
import com.vitorpamplona.amethyst.service.notifications.PushDistributorHandler
|
import com.vitorpamplona.amethyst.service.notifications.PushDistributorHandler
|
||||||
import com.vitorpamplona.amethyst.ui.components.SpinnerSelectionDialog
|
import com.vitorpamplona.amethyst.ui.components.SpinnerSelectionDialog
|
||||||
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
|
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.screen.loggedIn.settings.SettingsRow
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
@@ -65,12 +69,31 @@ import kotlinx.collections.immutable.toImmutableList
|
|||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesViewModel) {
|
fun SelectNotificationProvider(sharedPrefs: UiSettingsFlow) {
|
||||||
val notificationPermissionState =
|
val notificationPermissionGranted =
|
||||||
checkifItNeedsToRequestNotificationPermission(sharedPreferencesViewModel)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
val notificationPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
|
||||||
if (notificationPermissionState.status.isGranted) {
|
if (!notificationPermissionState.status.isGranted) {
|
||||||
if (!sharedPreferencesViewModel.sharedPrefs.dontShowPushNotificationSelector) {
|
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
|
val context = LocalContext.current
|
||||||
var distributorPresent by remember {
|
var distributorPresent by remember {
|
||||||
mutableStateOf(PushDistributorHandler.savedDistributorExists())
|
mutableStateOf(PushDistributorHandler.savedDistributorExists())
|
||||||
@@ -84,8 +107,8 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
|
|||||||
onSelect = { index ->
|
onSelect = { index ->
|
||||||
if (list[index] == "None") {
|
if (list[index] == "None") {
|
||||||
PushDistributorHandler.forceRemoveDistributor(context)
|
PushDistributorHandler.forceRemoveDistributor(context)
|
||||||
sharedPreferencesViewModel.dontAskForNotificationPermissions()
|
sharedPrefs.dontAskForNotificationPermissions()
|
||||||
sharedPreferencesViewModel.dontShowPushNotificationSelector()
|
sharedPrefs.dontShowPushNotificationSelector()
|
||||||
} else {
|
} else {
|
||||||
val fullDistributorName = list[index]
|
val fullDistributorName = list[index]
|
||||||
PushDistributorHandler.saveDistributor(fullDistributorName)
|
PushDistributorHandler.saveDistributor(fullDistributorName)
|
||||||
@@ -125,7 +148,7 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
|
|||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
distributorPresent = true
|
distributorPresent = true
|
||||||
sharedPreferencesViewModel.dontShowPushNotificationSelector()
|
sharedPrefs.dontShowPushNotificationSelector()
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text(stringRes(R.string.quick_action_dont_show_again_button))
|
Text(stringRes(R.string.quick_action_dont_show_again_button))
|
||||||
@@ -187,7 +210,7 @@ fun LoadDistributors(onInner: @Composable (String, ImmutableList<String>, Immuta
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PushNotificationSettingsRow(sharedPreferencesViewModel: SharedPreferencesViewModel) {
|
fun PushNotificationSettingsRow(sharedPrefs: UiSettingsFlow) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
LoadDistributors { currentDistributor, list, readableListWithExplainer ->
|
LoadDistributors { currentDistributor, list, readableListWithExplainer ->
|
||||||
@@ -198,8 +221,8 @@ fun PushNotificationSettingsRow(sharedPreferencesViewModel: SharedPreferencesVie
|
|||||||
selectedIndex = list.indexOf(currentDistributor),
|
selectedIndex = list.indexOf(currentDistributor),
|
||||||
) { index ->
|
) { index ->
|
||||||
if (list[index] == "None") {
|
if (list[index] == "None") {
|
||||||
sharedPreferencesViewModel.dontAskForNotificationPermissions()
|
sharedPrefs.dontAskForNotificationPermissions()
|
||||||
sharedPreferencesViewModel.dontShowPushNotificationSelector()
|
sharedPrefs.dontShowPushNotificationSelector()
|
||||||
PushDistributorHandler.forceRemoveDistributor(context)
|
PushDistributorHandler.forceRemoveDistributor(context)
|
||||||
} else {
|
} else {
|
||||||
PushDistributorHandler.saveDistributor(list[index])
|
PushDistributorHandler.saveDistributor(list[index])
|
||||||
|
|||||||
@@ -21,210 +21,33 @@
|
|||||||
package com.vitorpamplona.amethyst
|
package com.vitorpamplona.amethyst
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.util.Log
|
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.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() {
|
class Amethyst : Application() {
|
||||||
val appAgent = "Amethyst/${BuildConfig.VERSION_NAME}"
|
companion object {
|
||||||
|
lateinit var instance: AppModules
|
||||||
// Exists to avoid exceptions stopping the coroutine
|
private set
|
||||||
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() }
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
Log.d("AmethystApp", "onCreate $this")
|
Log.d("AmethystApp", "onCreate $this")
|
||||||
instance = this
|
instance = AppModules(this)
|
||||||
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler(UnexpectedCrashSaver(crashReportCache, applicationIOScope))
|
|
||||||
|
|
||||||
if (isDebug) {
|
if (isDebug) {
|
||||||
Logging.setup()
|
Logging.setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
// initializes diskcache on an IO thread.
|
instance.initiate(this)
|
||||||
applicationIOScope.launch {
|
|
||||||
diskCache
|
|
||||||
videoCache
|
|
||||||
}
|
|
||||||
|
|
||||||
// registers to receive events
|
|
||||||
pokeyReceiver.register(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTerminate() {
|
override fun onTerminate() {
|
||||||
super.onTerminate()
|
super.onTerminate()
|
||||||
Log.d("AmethystApp", "onTerminate $this")
|
Log.d("AmethystApp", "onTerminate $this")
|
||||||
|
instance.terminate(this)
|
||||||
pokeyReceiver.unregister(this)
|
|
||||||
applicationIOScope.cancel("Application onTerminate $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.
|
* 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) {
|
override fun onTrimMemory(level: Int) {
|
||||||
super.onTrimMemory(level)
|
super.onTrimMemory(level)
|
||||||
Log.d("AmethystApp", "onTrimMemory $level")
|
Log.d("AmethystApp", "onTrimMemory $level")
|
||||||
applicationIOScope.launch(Dispatchers.Default) {
|
instance.trim()
|
||||||
trimmingService.run(null, LocalPreferences.allSavedAccounts())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
lateinit var instance: Amethyst
|
|
||||||
private set
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.ALL_FOLLOWS
|
||||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
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.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
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.experimental.ephemChat.list.EphemeralChatListEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
@@ -210,7 +208,7 @@ object LocalPreferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val prefsDirPath: String
|
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) {
|
private suspend fun addAccount(accInfo: AccountInfo) {
|
||||||
val accounts = savedAccounts().filter { it.npub != accInfo.npub }.plus(accInfo)
|
val accounts = savedAccounts().filter { it.npub != accInfo.npub }.plus(accInfo)
|
||||||
@@ -255,7 +253,7 @@ object LocalPreferences {
|
|||||||
return if (BuildConfig.DEBUG && DEBUG_PLAINTEXT_PREFERENCES) {
|
return if (BuildConfig.DEBUG && DEBUG_PLAINTEXT_PREFERENCES) {
|
||||||
val preferenceFile =
|
val preferenceFile =
|
||||||
if (npub == null) DEBUG_PREFERENCES_NAME else "${DEBUG_PREFERENCES_NAME}_$npub"
|
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 {
|
} else {
|
||||||
Amethyst.instance.encryptedStorage(npub)
|
Amethyst.instance.encryptedStorage(npub)
|
||||||
}
|
}
|
||||||
@@ -475,8 +473,6 @@ object LocalPreferences {
|
|||||||
remove(PrefKeys.USE_PROXY)
|
remove(PrefKeys.USE_PROXY)
|
||||||
remove(PrefKeys.PROXY_PORT)
|
remove(PrefKeys.PROXY_PORT)
|
||||||
|
|
||||||
putString(PrefKeys.TOR_SETTINGS, JsonMapper.mapper.writeValueAsString(settings.torSettings.toSettings()))
|
|
||||||
|
|
||||||
val regularMap =
|
val regularMap =
|
||||||
settings.lastReadPerRoute.value.mapValues {
|
settings.lastReadPerRoute.value.mapValues {
|
||||||
it.value.value
|
it.value.value
|
||||||
@@ -501,7 +497,7 @@ object LocalPreferences {
|
|||||||
suspend fun loadAccountConfigFromEncryptedStorage(): AccountSettings? = currentAccount()?.let { loadAccountConfigFromEncryptedStorage(it) }
|
suspend fun loadAccountConfigFromEncryptedStorage(): AccountSettings? = currentAccount()?.let { loadAccountConfigFromEncryptedStorage(it) }
|
||||||
|
|
||||||
suspend fun saveSharedSettings(
|
suspend fun saveSharedSettings(
|
||||||
sharedSettings: Settings,
|
sharedSettings: UiSettings,
|
||||||
prefs: SharedPreferences = encryptedPreferences(),
|
prefs: SharedPreferences = encryptedPreferences(),
|
||||||
) {
|
) {
|
||||||
Log.d("LocalPreferences", "Saving to shared settings")
|
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")
|
Log.d("LocalPreferences", "Load shared settings")
|
||||||
with(prefs) {
|
with(prefs) {
|
||||||
return try {
|
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) {
|
} catch (e: Throwable) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
Log.w(
|
Log.w(
|
||||||
@@ -598,8 +596,6 @@ object LocalPreferences {
|
|||||||
val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false)
|
val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false)
|
||||||
val hideNIP17WarningDialog = getBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, false)
|
val hideNIP17WarningDialog = getBoolean(PrefKeys.HIDE_NIP_17_WARNING_DIALOG, false)
|
||||||
|
|
||||||
val torSettings = parseOrNull<TorSettings>(PrefKeys.TOR_SETTINGS) ?: TorSettings()
|
|
||||||
|
|
||||||
val lastReadPerRoute =
|
val lastReadPerRoute =
|
||||||
parseOrNull<Map<String, Long>>(PrefKeys.LAST_READ_PER_ROUTE)?.mapValues {
|
parseOrNull<Map<String, Long>>(PrefKeys.LAST_READ_PER_ROUTE)?.mapValues {
|
||||||
MutableStateFlow(it.value)
|
MutableStateFlow(it.value)
|
||||||
@@ -637,7 +633,6 @@ object LocalPreferences {
|
|||||||
backupHashtagList = latestHashtagList,
|
backupHashtagList = latestHashtagList,
|
||||||
backupGeohashList = latestGeohashList,
|
backupGeohashList = latestGeohashList,
|
||||||
backupEphemeralChatList = latestEphemeralList,
|
backupEphemeralChatList = latestEphemeralList,
|
||||||
torSettings = TorSettingsFlow.build(torSettings),
|
|
||||||
lastReadPerRoute = MutableStateFlow(lastReadPerRoute),
|
lastReadPerRoute = MutableStateFlow(lastReadPerRoute),
|
||||||
hasDonatedInVersion = MutableStateFlow(hasDonatedInVersion),
|
hasDonatedInVersion = MutableStateFlow(hasDonatedInVersion),
|
||||||
pendingAttestations = MutableStateFlow(pendingAttestations),
|
pendingAttestations = MutableStateFlow(pendingAttestations),
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.model
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import com.vitorpamplona.amethyst.Amethyst
|
|
||||||
import com.vitorpamplona.amethyst.BuildConfig
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
import com.vitorpamplona.amethyst.LocalPreferences
|
import com.vitorpamplona.amethyst.LocalPreferences
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
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.nip78AppSpecific.AppSpecificState
|
||||||
import com.vitorpamplona.amethyst.model.nip96FileStorage.FileStorageServerListState
|
import com.vitorpamplona.amethyst.model.nip96FileStorage.FileStorageServerListState
|
||||||
import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState
|
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.MergedFollowListsState
|
||||||
import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineRelayListsState
|
import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineRelayListsState
|
||||||
import com.vitorpamplona.amethyst.model.serverList.MergedServerListState
|
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.FeedTopNavFilterState
|
||||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
|
||||||
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxLoaderState
|
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.location.LocationState
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.service.uploads.FileHeader
|
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.hasAnyTaggedUser
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
|
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.references.references
|
import com.vitorpamplona.quartz.nip01Core.tags.references.references
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
|
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OkHttpOtsResolverBuilder
|
|
||||||
import com.vitorpamplona.quartz.nip04Dm.PrivateDMCache
|
import com.vitorpamplona.quartz.nip04Dm.PrivateDMCache
|
||||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||||
@@ -226,7 +222,7 @@ class Account(
|
|||||||
val signer: NostrSigner,
|
val signer: NostrSigner,
|
||||||
val geolocationFlow: StateFlow<LocationState.LocationResult>,
|
val geolocationFlow: StateFlow<LocationState.LocationResult>,
|
||||||
val nwcFilterAssembler: NWCPaymentFilterAssembler,
|
val nwcFilterAssembler: NWCPaymentFilterAssembler,
|
||||||
val otsVerifCache: VerificationStateCache,
|
val otsResolverBuilder: OtsResolverBuilder,
|
||||||
val cache: LocalCache,
|
val cache: LocalCache,
|
||||||
val client: INostrClient,
|
val client: INostrClient,
|
||||||
val scope: CoroutineScope,
|
val scope: CoroutineScope,
|
||||||
@@ -323,17 +319,6 @@ class Account(
|
|||||||
|
|
||||||
val chatroomList = cache.getOrCreateChatroomList(signer.pubKey)
|
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 newNotesPreProcessor = EventProcessor(this, cache)
|
||||||
|
|
||||||
val otsState = OtsState(signer, cache, otsResolverBuilder, scope, settings)
|
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.DEFAULT_MEDIA_SERVERS
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||||
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
|
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.experimental.ephemChat.list.EphemeralChatListEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||||
@@ -132,7 +130,6 @@ class AccountSettings(
|
|||||||
var backupHashtagList: HashtagListEvent? = null,
|
var backupHashtagList: HashtagListEvent? = null,
|
||||||
var backupGeohashList: GeohashListEvent? = null,
|
var backupGeohashList: GeohashListEvent? = null,
|
||||||
var backupEphemeralChatList: EphemeralChatListEvent? = null,
|
var backupEphemeralChatList: EphemeralChatListEvent? = null,
|
||||||
val torSettings: TorSettingsFlow = TorSettingsFlow(),
|
|
||||||
val lastReadPerRoute: MutableStateFlow<Map<String, MutableStateFlow<Long>>> = MutableStateFlow(mapOf()),
|
val lastReadPerRoute: MutableStateFlow<Map<String, MutableStateFlow<Long>>> = MutableStateFlow(mapOf()),
|
||||||
var hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf<String>()),
|
var hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf<String>()),
|
||||||
val pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
|
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
|
// language services
|
||||||
// ---
|
// ---
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ import androidx.compose.runtime.Stable
|
|||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
data class Settings(
|
data class UiSettings(
|
||||||
val theme: ThemeType = ThemeType.SYSTEM,
|
val theme: ThemeType = ThemeType.SYSTEM,
|
||||||
val preferredLanguage: String? = null,
|
val preferredLanguage: String? = null,
|
||||||
val automaticallyShowImages: ConnectivityType = ConnectivityType.ALWAYS,
|
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.relay.client.INostrClient
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
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 com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -47,6 +47,7 @@ class AccountCacheState(
|
|||||||
val geolocationFlow: StateFlow<LocationState.LocationResult>,
|
val geolocationFlow: StateFlow<LocationState.LocationResult>,
|
||||||
val nwcFilterAssembler: NWCPaymentFilterAssembler,
|
val nwcFilterAssembler: NWCPaymentFilterAssembler,
|
||||||
val contentResolverFn: () -> ContentResolver,
|
val contentResolverFn: () -> ContentResolver,
|
||||||
|
val otsResolverBuilder: OtsResolverBuilder,
|
||||||
val cache: LocalCache,
|
val cache: LocalCache,
|
||||||
val client: INostrClient,
|
val client: INostrClient,
|
||||||
) {
|
) {
|
||||||
@@ -91,7 +92,7 @@ class AccountCacheState(
|
|||||||
signer = signer,
|
signer = signer,
|
||||||
geolocationFlow = geolocationFlow,
|
geolocationFlow = geolocationFlow,
|
||||||
nwcFilterAssembler = nwcFilterAssembler,
|
nwcFilterAssembler = nwcFilterAssembler,
|
||||||
otsVerifCache = VerificationStateCache(),
|
otsResolverBuilder = otsResolverBuilder,
|
||||||
cache = cache,
|
cache = cache,
|
||||||
client = client,
|
client = client,
|
||||||
scope =
|
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
|
* 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.
|
* 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 android.util.LruCache
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp
|
||||||
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
|
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.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 relayInformationEmptyCache = LruCache<NormalizedRelayUrl, Nip11RelayInformation>(1000)
|
||||||
private val relayInformationDocumentCache = LruCache<NormalizedRelayUrl, RetrieveResult?>(1000)
|
private val relayInformationDocumentCache = LruCache<NormalizedRelayUrl, RetrieveResult?>(1000)
|
||||||
private val retriever = Nip11Retriever()
|
private val retriever = Nip11Retriever(okHttpClient)
|
||||||
|
|
||||||
fun getEmpty(relay: NormalizedRelayUrl): Nip11RelayInformation {
|
fun getEmpty(relay: NormalizedRelayUrl): Nip11RelayInformation {
|
||||||
relayInformationEmptyCache.get(relay)?.let { return it }
|
relayInformationEmptyCache.get(relay)?.let { return it }
|
||||||
@@ -98,7 +68,6 @@ object Nip11CachedRetriever {
|
|||||||
|
|
||||||
suspend fun loadRelayInfo(
|
suspend fun loadRelayInfo(
|
||||||
relay: NormalizedRelayUrl,
|
relay: NormalizedRelayUrl,
|
||||||
okHttpClient: (String) -> OkHttpClient,
|
|
||||||
onInfo: (Nip11RelayInformation) -> Unit,
|
onInfo: (Nip11RelayInformation) -> Unit,
|
||||||
onError: (NormalizedRelayUrl, Nip11Retriever.ErrorCode, String?) -> Unit,
|
onError: (NormalizedRelayUrl, Nip11Retriever.ErrorCode, String?) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -110,33 +79,31 @@ object Nip11CachedRetriever {
|
|||||||
if (doc.isValid()) {
|
if (doc.isValid()) {
|
||||||
// just wait.
|
// just wait.
|
||||||
} else {
|
} else {
|
||||||
retrieve(relay, okHttpClient, onInfo, onError)
|
retrieve(relay, onInfo, onError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is RetrieveResult.Error -> {
|
is RetrieveResult.Error -> {
|
||||||
if (doc.isValid()) {
|
if (doc.isValid()) {
|
||||||
onError(relay, doc.error, null)
|
onError(relay, doc.error, null)
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
retrieve(relay, okHttpClient, onInfo, onError)
|
retrieve(relay, onInfo, onError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun retrieve(
|
private suspend fun retrieve(
|
||||||
relay: NormalizedRelayUrl,
|
relay: NormalizedRelayUrl,
|
||||||
okHttpClient: (String) -> OkHttpClient,
|
|
||||||
onInfo: (Nip11RelayInformation) -> Unit,
|
onInfo: (Nip11RelayInformation) -> Unit,
|
||||||
onError: (NormalizedRelayUrl, Nip11Retriever.ErrorCode, String?) -> Unit,
|
onError: (NormalizedRelayUrl, Nip11Retriever.ErrorCode, String?) -> Unit,
|
||||||
) {
|
) {
|
||||||
relayInformationDocumentCache.put(relay, RetrieveResult.Loading(getEmpty(relay)))
|
relayInformationDocumentCache.put(relay, RetrieveResult.Loading(getEmpty(relay)))
|
||||||
retriever.loadRelayInfo(
|
retriever.loadRelayInfo(
|
||||||
relay = relay,
|
relay = relay,
|
||||||
okHttpClient = okHttpClient,
|
|
||||||
onInfo = {
|
onInfo = {
|
||||||
relayInformationDocumentCache.put(relay, RetrieveResult.Success(it))
|
relayInformationDocumentCache.put(relay, RetrieveResult.Success(it))
|
||||||
relayInformationEmptyCache.remove(relay)
|
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
|
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.amethyst.ui.tor.TorType
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
class PrivacyState(
|
class RoleBasedHttpClientBuilder(
|
||||||
val settings: AccountSettings,
|
val okHttpClient: DualHttpClientManager,
|
||||||
) {
|
val torSettings: TorSettingsFlow,
|
||||||
|
) : IRoleBasedHttpClientBuilder {
|
||||||
fun shouldUseTorForImageDownload(url: String) =
|
fun shouldUseTorForImageDownload(url: String) =
|
||||||
shouldUseTorFor(
|
shouldUseTorFor(
|
||||||
url,
|
url,
|
||||||
settings.torSettings.torType.value,
|
torSettings.torType.value,
|
||||||
settings.torSettings.imagesViaTor.value,
|
torSettings.imagesViaTor.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun shouldUseTorFor(
|
fun shouldUseTorFor(
|
||||||
@@ -57,37 +60,37 @@ class PrivacyState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun shouldUseTorForVideoDownload() =
|
fun shouldUseTorForVideoDownload() =
|
||||||
when (settings.torSettings.torType.value) {
|
when (torSettings.torType.value) {
|
||||||
TorType.OFF -> false
|
TorType.OFF -> false
|
||||||
TorType.INTERNAL -> settings.torSettings.videosViaTor.value
|
TorType.INTERNAL -> torSettings.videosViaTor.value
|
||||||
TorType.EXTERNAL -> settings.torSettings.videosViaTor.value
|
TorType.EXTERNAL -> torSettings.videosViaTor.value
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shouldUseTorForVideoDownload(url: String) =
|
fun shouldUseTorForVideoDownload(url: String) =
|
||||||
when (settings.torSettings.torType.value) {
|
when (torSettings.torType.value) {
|
||||||
TorType.OFF -> false
|
TorType.OFF -> false
|
||||||
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.videosViaTor.value)
|
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, torSettings.videosViaTor.value)
|
||||||
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.videosViaTor.value)
|
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, torSettings.videosViaTor.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shouldUseTorForPreviewUrl(url: String) =
|
fun shouldUseTorForPreviewUrl(url: String) =
|
||||||
when (settings.torSettings.torType.value) {
|
when (torSettings.torType.value) {
|
||||||
TorType.OFF -> false
|
TorType.OFF -> false
|
||||||
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.urlPreviewsViaTor.value)
|
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, torSettings.urlPreviewsViaTor.value)
|
||||||
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.urlPreviewsViaTor.value)
|
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, torSettings.urlPreviewsViaTor.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shouldUseTorForTrustedRelays() =
|
fun shouldUseTorForTrustedRelays() =
|
||||||
when (settings.torSettings.torType.value) {
|
when (torSettings.torType.value) {
|
||||||
TorType.OFF -> false
|
TorType.OFF -> false
|
||||||
TorType.INTERNAL -> settings.torSettings.trustedRelaysViaTor.value
|
TorType.INTERNAL -> torSettings.trustedRelaysViaTor.value
|
||||||
TorType.EXTERNAL -> settings.torSettings.trustedRelaysViaTor.value
|
TorType.EXTERNAL -> torSettings.trustedRelaysViaTor.value
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkLocalHostOnionAndThen(
|
private fun checkLocalHostOnionAndThen(
|
||||||
url: String,
|
url: String,
|
||||||
final: Boolean,
|
final: Boolean,
|
||||||
): Boolean = checkLocalHostOnionAndThen(url, settings.torSettings.onionRelaysViaTor.value, final)
|
): Boolean = checkLocalHostOnionAndThen(url, torSettings.onionRelaysViaTor.value, final)
|
||||||
|
|
||||||
private fun checkLocalHostOnionAndThen(
|
private fun checkLocalHostOnionAndThen(
|
||||||
normalizedUrl: String,
|
normalizedUrl: String,
|
||||||
@@ -103,23 +106,39 @@ class PrivacyState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun shouldUseTorForMoneyOperations(url: String) =
|
fun shouldUseTorForMoneyOperations(url: String) =
|
||||||
when (settings.torSettings.torType.value) {
|
when (torSettings.torType.value) {
|
||||||
TorType.OFF -> false
|
TorType.OFF -> false
|
||||||
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.moneyOperationsViaTor.value)
|
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, torSettings.moneyOperationsViaTor.value)
|
||||||
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.moneyOperationsViaTor.value)
|
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, torSettings.moneyOperationsViaTor.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shouldUseTorForNIP05(url: String) =
|
fun shouldUseTorForNIP05(url: String) =
|
||||||
when (settings.torSettings.torType.value) {
|
when (torSettings.torType.value) {
|
||||||
TorType.OFF -> false
|
TorType.OFF -> false
|
||||||
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip05VerificationsViaTor.value)
|
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, torSettings.nip05VerificationsViaTor.value)
|
||||||
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip05VerificationsViaTor.value)
|
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, torSettings.nip05VerificationsViaTor.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shouldUseTorForUploads(url: String) =
|
fun shouldUseTorForUploads(url: String) =
|
||||||
when (settings.torSettings.torType.value) {
|
when (torSettings.torType.value) {
|
||||||
TorType.OFF -> false
|
TorType.OFF -> false
|
||||||
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip96UploadsViaTor.value)
|
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, torSettings.nip96UploadsViaTor.value)
|
||||||
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.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
|
package com.vitorpamplona.amethyst.model.torState
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager
|
||||||
import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState
|
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
||||||
import com.vitorpamplona.amethyst.model.serverList.TrustedRelayListsState
|
|
||||||
import com.vitorpamplona.amethyst.ui.tor.TorType
|
import com.vitorpamplona.amethyst.ui.tor.TorType
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.combineTransform
|
import kotlinx.coroutines.flow.combineTransform
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
class TorRelayState(
|
class TorRelayState(
|
||||||
val trustedRelayState: TrustedRelayListsState,
|
val okHttpClient: DualHttpClientManager,
|
||||||
val dmRelayState: DmRelayListState,
|
val torSettingsFlow: TorSettingsFlow,
|
||||||
val settings: AccountSettings,
|
|
||||||
val scope: CoroutineScope,
|
val scope: CoroutineScope,
|
||||||
) {
|
) {
|
||||||
|
val dmRelays = MutableStateFlow<Set<NormalizedRelayUrl>>(emptySet())
|
||||||
|
val trustedRelays = MutableStateFlow<Set<NormalizedRelayUrl>>(emptySet())
|
||||||
|
|
||||||
val torSettings =
|
val torSettings =
|
||||||
combine(
|
combine(
|
||||||
settings.torSettings.torType,
|
torSettingsFlow.torType,
|
||||||
settings.torSettings.onionRelaysViaTor,
|
torSettingsFlow.onionRelaysViaTor,
|
||||||
settings.torSettings.dmRelaysViaTor,
|
torSettingsFlow.dmRelaysViaTor,
|
||||||
settings.torSettings.trustedRelaysViaTor,
|
torSettingsFlow.trustedRelaysViaTor,
|
||||||
settings.torSettings.newRelaysViaTor,
|
torSettingsFlow.newRelaysViaTor,
|
||||||
) {
|
) {
|
||||||
torType: TorType,
|
torType: TorType,
|
||||||
onionRelaysViaTor: Boolean,
|
onionRelaysViaTor: Boolean,
|
||||||
@@ -64,11 +67,11 @@ class TorRelayState(
|
|||||||
}.onStart {
|
}.onStart {
|
||||||
emit(
|
emit(
|
||||||
TorRelaySettings(
|
TorRelaySettings(
|
||||||
torType = settings.torSettings.torType.value,
|
torType = torSettingsFlow.torType.value,
|
||||||
onionRelaysViaTor = settings.torSettings.onionRelaysViaTor.value,
|
onionRelaysViaTor = torSettingsFlow.onionRelaysViaTor.value,
|
||||||
dmRelaysViaTor = settings.torSettings.dmRelaysViaTor.value,
|
dmRelaysViaTor = torSettingsFlow.dmRelaysViaTor.value,
|
||||||
trustedRelaysViaTor = settings.torSettings.trustedRelaysViaTor.value,
|
trustedRelaysViaTor = torSettingsFlow.trustedRelaysViaTor.value,
|
||||||
newRelaysViaTor = settings.torSettings.newRelaysViaTor.value,
|
newRelaysViaTor = torSettingsFlow.newRelaysViaTor.value,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}.flowOn(Dispatchers.Default)
|
}.flowOn(Dispatchers.Default)
|
||||||
@@ -76,20 +79,21 @@ class TorRelayState(
|
|||||||
scope,
|
scope,
|
||||||
SharingStarted.Eagerly,
|
SharingStarted.Eagerly,
|
||||||
TorRelaySettings(
|
TorRelaySettings(
|
||||||
torType = settings.torSettings.torType.value,
|
torType = torSettingsFlow.torType.value,
|
||||||
onionRelaysViaTor = settings.torSettings.onionRelaysViaTor.value,
|
onionRelaysViaTor = torSettingsFlow.onionRelaysViaTor.value,
|
||||||
dmRelaysViaTor = settings.torSettings.dmRelaysViaTor.value,
|
dmRelaysViaTor = torSettingsFlow.dmRelaysViaTor.value,
|
||||||
trustedRelaysViaTor = settings.torSettings.trustedRelaysViaTor.value,
|
trustedRelaysViaTor = torSettingsFlow.trustedRelaysViaTor.value,
|
||||||
newRelaysViaTor = settings.torSettings.newRelaysViaTor.value,
|
newRelaysViaTor = torSettingsFlow.newRelaysViaTor.value,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
val flow =
|
val flow =
|
||||||
combineTransform(
|
combineTransform(
|
||||||
torSettings,
|
torSettings,
|
||||||
trustedRelayState.flow,
|
trustedRelays,
|
||||||
dmRelayState.flow,
|
dmRelays,
|
||||||
) { torSettings: TorRelaySettings, trustedRelayList: Set<NormalizedRelayUrl>, dmRelayList: Set<NormalizedRelayUrl> ->
|
) { torSettings: TorRelaySettings, trustedRelayList: Set<NormalizedRelayUrl>, dmRelayList: Set<NormalizedRelayUrl> ->
|
||||||
|
println("AABBCC New tor evaluation: $torSettings $trustedRelayList $dmRelayList")
|
||||||
emit(
|
emit(
|
||||||
TorRelayEvaluation(
|
TorRelayEvaluation(
|
||||||
torSettings = torSettings,
|
torSettings = torSettings,
|
||||||
@@ -101,8 +105,8 @@ class TorRelayState(
|
|||||||
emit(
|
emit(
|
||||||
TorRelayEvaluation(
|
TorRelayEvaluation(
|
||||||
torSettings = torSettings.value,
|
torSettings = torSettings.value,
|
||||||
trustedRelayList = trustedRelayState.flow.value,
|
trustedRelayList = trustedRelays.value,
|
||||||
dmRelayList = dmRelayState.flow.value,
|
dmRelayList = dmRelays.value,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}.flowOn(Dispatchers.Default)
|
}.flowOn(Dispatchers.Default)
|
||||||
@@ -111,10 +115,12 @@ class TorRelayState(
|
|||||||
SharingStarted.Eagerly,
|
SharingStarted.Eagerly,
|
||||||
TorRelayEvaluation(
|
TorRelayEvaluation(
|
||||||
torSettings = torSettings.value,
|
torSettings = torSettings.value,
|
||||||
trustedRelayList = trustedRelayState.flow.value,
|
trustedRelayList = trustedRelays.value,
|
||||||
dmRelayList = dmRelayState.flow.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
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
import android.app.Application
|
import android.content.Context
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
fun Application.safeCacheDir(): File {
|
fun Context.safeCacheDir(): File {
|
||||||
val cacheDir = checkNotNull(cacheDir) { "cacheDir == null" }
|
val cacheDir = checkNotNull(cacheDir) { "cacheDir == null" }
|
||||||
return cacheDir.apply { mkdirs() }
|
return cacheDir.apply { mkdirs() }
|
||||||
}
|
}
|
||||||
+1
-1
@@ -39,7 +39,7 @@ class ConnectivityFlow(
|
|||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
val status =
|
val status =
|
||||||
callbackFlow {
|
callbackFlow {
|
||||||
trySend(ConnectivityStatus.Connecting)
|
trySend(ConnectivityStatus.StartingService)
|
||||||
|
|
||||||
val connectivityManager = context.getConnectivityManager()
|
val connectivityManager = context.getConnectivityManager()
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ class ConnectivityManager(
|
|||||||
ConnectivityFlow(context).status.distinctUntilChanged().stateIn(
|
ConnectivityFlow(context).status.distinctUntilChanged().stateIn(
|
||||||
scope,
|
scope,
|
||||||
SharingStarted.WhileSubscribed(30000),
|
SharingStarted.WhileSubscribed(30000),
|
||||||
ConnectivityStatus.Off,
|
ConnectivityStatus.StartingService,
|
||||||
)
|
)
|
||||||
|
|
||||||
val isMobileOrNull: StateFlow<Boolean?> =
|
val isMobileOrNull: StateFlow<Boolean?> =
|
||||||
|
|||||||
+1
-1
@@ -28,5 +28,5 @@ sealed class ConnectivityStatus {
|
|||||||
|
|
||||||
object Off : ConnectivityStatus()
|
object Off : ConnectivityStatus()
|
||||||
|
|
||||||
object Connecting : ConnectivityStatus()
|
object StartingService : ConnectivityStatus()
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-10
@@ -20,7 +20,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.service.notifications
|
package com.vitorpamplona.amethyst.service.notifications
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.RECEIVER_EXPORTED
|
import android.content.Context.RECEIVER_EXPORTED
|
||||||
@@ -28,8 +27,12 @@ import android.content.Intent
|
|||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.vitorpamplona.amethyst.Amethyst
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
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
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class PokeyReceiver : BroadcastReceiver() {
|
class PokeyReceiver : BroadcastReceiver() {
|
||||||
@@ -39,7 +42,14 @@ class PokeyReceiver : BroadcastReceiver() {
|
|||||||
val INTENT = IntentFilter(POKEY_ACTION)
|
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) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
app.registerReceiver(this, INTENT, RECEIVER_EXPORTED)
|
app.registerReceiver(this, INTENT, RECEIVER_EXPORTED)
|
||||||
} else {
|
} else {
|
||||||
@@ -48,8 +58,9 @@ class PokeyReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unregister(app: Application) {
|
fun unregister(app: Context) {
|
||||||
app.unregisterReceiver(this)
|
app.unregisterReceiver(this)
|
||||||
|
scope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceive(
|
override fun onReceive(
|
||||||
@@ -62,13 +73,9 @@ class PokeyReceiver : BroadcastReceiver() {
|
|||||||
|
|
||||||
if (eventStr == null) return
|
if (eventStr == null) return
|
||||||
|
|
||||||
val app = context.applicationContext as Amethyst
|
scope.launch {
|
||||||
|
|
||||||
app.applicationIOScope.launch {
|
|
||||||
try {
|
try {
|
||||||
EventNotificationConsumer(app).findAccountAndConsume(
|
EventNotificationConsumer(context.applicationContext).findAccountAndConsume(Event.fromJson(eventStr))
|
||||||
Event.fromJson(eventStr),
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to parse Pokey Event", e)
|
Log.e(TAG, "Failed to parse Pokey Event", e)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ fun VideoViewInner(
|
|||||||
callbackUri = nostrUriCallback,
|
callbackUri = nostrUriCallback,
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
aspectRatio = aspectRatio,
|
aspectRatio = aspectRatio,
|
||||||
proxyPort = accountViewModel.proxyPortForVideo(videoUri),
|
proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(videoUri),
|
||||||
keepPlaying = true,
|
keepPlaying = true,
|
||||||
waveformData = waveform,
|
waveformData = waveform,
|
||||||
) { mediaItem ->
|
) { 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.ConnectivityManager
|
||||||
import com.vitorpamplona.amethyst.service.connectivity.ConnectivityStatus
|
import com.vitorpamplona.amethyst.service.connectivity.ConnectivityStatus
|
||||||
import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager
|
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.TorManager
|
||||||
import com.vitorpamplona.amethyst.ui.tor.TorServiceStatus
|
import com.vitorpamplona.amethyst.ui.tor.TorServiceStatus
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||||
@@ -45,7 +44,7 @@ import net.freehaven.tor.control.TorControlCommands
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
class RelayProxyClientConnector(
|
class RelayProxyClientConnector(
|
||||||
val torProxySettingsAnchor: ProxySettingsAnchor,
|
val torEvaluator: StateFlow<TorRelayEvaluation>,
|
||||||
val okHttpClients: DualHttpClientManager,
|
val okHttpClients: DualHttpClientManager,
|
||||||
val connManager: ConnectivityManager,
|
val connManager: ConnectivityManager,
|
||||||
val client: INostrClient,
|
val client: INostrClient,
|
||||||
@@ -53,7 +52,7 @@ class RelayProxyClientConnector(
|
|||||||
val scope: CoroutineScope,
|
val scope: CoroutineScope,
|
||||||
) {
|
) {
|
||||||
data class RelayServiceInfra(
|
data class RelayServiceInfra(
|
||||||
val torSettings: StateFlow<TorRelayEvaluation>,
|
val evaluator: TorRelayEvaluation,
|
||||||
val torConnection: OkHttpClient,
|
val torConnection: OkHttpClient,
|
||||||
val clearConnection: OkHttpClient,
|
val clearConnection: OkHttpClient,
|
||||||
val connectivity: ConnectivityStatus,
|
val connectivity: ConnectivityStatus,
|
||||||
@@ -62,7 +61,7 @@ class RelayProxyClientConnector(
|
|||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
val relayServices =
|
val relayServices =
|
||||||
combine(
|
combine(
|
||||||
torProxySettingsAnchor.flow,
|
torEvaluator,
|
||||||
okHttpClients.defaultHttpClient,
|
okHttpClients.defaultHttpClient,
|
||||||
okHttpClients.defaultHttpClientWithoutProxy,
|
okHttpClients.defaultHttpClientWithoutProxy,
|
||||||
connManager.status,
|
connManager.status,
|
||||||
@@ -70,7 +69,9 @@ class RelayProxyClientConnector(
|
|||||||
RelayServiceInfra(torSettings, torConnection, clearConnection, connectivity)
|
RelayServiceInfra(torSettings, torConnection, clearConnection, connectivity)
|
||||||
}.debounce(100)
|
}.debounce(100)
|
||||||
.onEach {
|
.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}")
|
Log.d("ManageRelayServices", "Pausing Relay Services ${it.connectivity}")
|
||||||
if (client.isActive()) {
|
if (client.isActive()) {
|
||||||
client.disconnect()
|
client.disconnect()
|
||||||
|
|||||||
+4
-4
@@ -152,7 +152,7 @@ class UploadOrchestrator {
|
|||||||
alt = alt,
|
alt = alt,
|
||||||
sensitiveContent = contentWarningReason,
|
sensitiveContent = contentWarningReason,
|
||||||
serverBaseUrl = serverBaseUrl,
|
serverBaseUrl = serverBaseUrl,
|
||||||
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) },
|
okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads,
|
||||||
onProgress = { percent: Float ->
|
onProgress = { percent: Float ->
|
||||||
updateState(0.2 + (0.2 * percent), UploadingState.Uploading)
|
updateState(0.2 + (0.2 * percent), UploadingState.Uploading)
|
||||||
},
|
},
|
||||||
@@ -165,7 +165,7 @@ class UploadOrchestrator {
|
|||||||
localContentType = contentType,
|
localContentType = contentType,
|
||||||
originalContentType = contentTypeForResult,
|
originalContentType = contentTypeForResult,
|
||||||
originalHash = originalHash,
|
originalHash = originalHash,
|
||||||
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) },
|
okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads,
|
||||||
)
|
)
|
||||||
} catch (_: SignerExceptions.ReadOnlyException) {
|
} catch (_: SignerExceptions.ReadOnlyException) {
|
||||||
error(R.string.login_with_a_private_key_to_be_able_to_upload)
|
error(R.string.login_with_a_private_key_to_be_able_to_upload)
|
||||||
@@ -198,7 +198,7 @@ class UploadOrchestrator {
|
|||||||
alt = alt,
|
alt = alt,
|
||||||
sensitiveContent = contentWarningReason,
|
sensitiveContent = contentWarningReason,
|
||||||
serverBaseUrl = serverBaseUrl,
|
serverBaseUrl = serverBaseUrl,
|
||||||
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) },
|
okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads,
|
||||||
httpAuth = account::createBlossomUploadAuth,
|
httpAuth = account::createBlossomUploadAuth,
|
||||||
context = context,
|
context = context,
|
||||||
)
|
)
|
||||||
@@ -206,7 +206,7 @@ class UploadOrchestrator {
|
|||||||
verifyHeader(
|
verifyHeader(
|
||||||
uploadResult = result,
|
uploadResult = result,
|
||||||
localContentType = contentType,
|
localContentType = contentType,
|
||||||
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) },
|
okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads,
|
||||||
originalHash = originalHash,
|
originalHash = originalHash,
|
||||||
originalContentType = contentTypeForResult,
|
originalContentType = contentTypeForResult,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import androidx.activity.enableEdgeToEdge
|
|||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.debugState
|
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.navigation.routes.routeFor
|
||||||
import com.vitorpamplona.amethyst.ui.screen.AccountScreen
|
import com.vitorpamplona.amethyst.ui.screen.AccountScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.prepareSharedViewModel
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||||
@@ -67,16 +67,14 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
StringResSetup()
|
StringResSetup()
|
||||||
|
AmethystTheme {
|
||||||
val sharedPreferencesViewModel = prepareSharedViewModel()
|
|
||||||
AmethystTheme(sharedPreferencesViewModel.sharedPrefs.theme) {
|
|
||||||
val accountStateViewModel: AccountStateViewModel = viewModel()
|
val accountStateViewModel: AccountStateViewModel = viewModel()
|
||||||
|
|
||||||
LaunchedEffect(key1 = Unit) {
|
LaunchedEffect(key1 = Unit) {
|
||||||
accountStateViewModel.loginWithDefaultAccountIfLoggedOff()
|
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.Modifier
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.util.fastForEach
|
import androidx.compose.ui.util.fastForEach
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -50,7 +49,7 @@ fun <T> CrossfadeIfEnabled(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
content: @Composable (T) -> Unit,
|
content: @Composable (T) -> Unit,
|
||||||
) {
|
) {
|
||||||
if (accountViewModel.settings.featureSet == FeatureSetType.PERFORMANCE) {
|
if (accountViewModel.settings.isPerformanceMode()) {
|
||||||
Box(modifier, contentAlignment) {
|
Box(modifier, contentAlignment) {
|
||||||
content(targetState)
|
content(targetState)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -193,7 +193,7 @@ class NewUserMetadataViewModel : ViewModel() {
|
|||||||
alt = null,
|
alt = null,
|
||||||
sensitiveContent = null,
|
sensitiveContent = null,
|
||||||
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
|
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
|
||||||
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) },
|
okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads,
|
||||||
onProgress = {},
|
onProgress = {},
|
||||||
httpAuth = account::createHTTPAuthorization,
|
httpAuth = account::createHTTPAuthorization,
|
||||||
context = context,
|
context = context,
|
||||||
@@ -206,7 +206,7 @@ class NewUserMetadataViewModel : ViewModel() {
|
|||||||
alt = null,
|
alt = null,
|
||||||
sensitiveContent = null,
|
sensitiveContent = null,
|
||||||
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
|
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
|
||||||
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) },
|
okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads,
|
||||||
httpAuth = account::createBlossomUploadAuth,
|
httpAuth = account::createBlossomUploadAuth,
|
||||||
context = context,
|
context = context,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -56,6 +56,6 @@ tailrec fun Context.getActivity(): ComponentActivity =
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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,
|
mimeType = content.mimeType,
|
||||||
okHttpClient = {
|
okHttpClient = {
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
accountViewModel.okHttpClientForImage(it)
|
accountViewModel.httpClientBuilder.okHttpClientForImage(it)
|
||||||
} else {
|
} else {
|
||||||
accountViewModel.okHttpClientForVideo(it)
|
accountViewModel.httpClientBuilder.okHttpClientForVideo(it)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
localContext,
|
localContext,
|
||||||
|
|||||||
+2
-3
@@ -52,7 +52,6 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import com.vitorpamplona.amethyst.model.BooleanType
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
@@ -70,7 +69,7 @@ fun DisappearingScaffold(
|
|||||||
val shouldShow = remember { mutableStateOf(isActive()) }
|
val shouldShow = remember { mutableStateOf(isActive()) }
|
||||||
|
|
||||||
val modifier =
|
val modifier =
|
||||||
if (accountViewModel.settings.automaticallyHideNavigationBars == BooleanType.ALWAYS) {
|
if (accountViewModel.settings.isImmersiveScrollingActive()) {
|
||||||
val bottomBarHeightPx = with(LocalDensity.current) { 50.dp.roundToPx().toFloat() }
|
val bottomBarHeightPx = with(LocalDensity.current) { 50.dp.roundToPx().toFloat() }
|
||||||
val bottomBarOffsetHeightPx = remember { mutableFloatStateOf(0f) }
|
val bottomBarOffsetHeightPx = remember { mutableFloatStateOf(0f) }
|
||||||
|
|
||||||
@@ -90,7 +89,7 @@ fun DisappearingScaffold(
|
|||||||
|
|
||||||
val newOffset = bottomBarOffsetHeightPx.floatValue + available.y
|
val newOffset = bottomBarOffsetHeightPx.floatValue + available.y
|
||||||
|
|
||||||
if (accountViewModel.settings.automaticallyHideNavigationBars == BooleanType.ALWAYS) {
|
if (accountViewModel.settings.isImmersiveScrollingActive()) {
|
||||||
val newBottomBarOffset =
|
val newBottomBarOffset =
|
||||||
if (!isInvertedLayout) {
|
if (!isInvertedLayout) {
|
||||||
newOffset.coerceIn(-bottomBarHeightPx, 0f)
|
newOffset.coerceIn(-bottomBarHeightPx, 0f)
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import androidx.core.net.toUri
|
|||||||
import androidx.core.util.Consumer
|
import androidx.core.util.Consumer
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.service.crashreports.DisplayCrashMessages
|
import com.vitorpamplona.amethyst.service.crashreports.DisplayCrashMessages
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.compose.DisplayNotifyMessages
|
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.navigation.routes.isSameRoute
|
||||||
import com.vitorpamplona.amethyst.ui.note.nip22Comments.ReplyCommentPostScreen
|
import com.vitorpamplona.amethyst.ui.note.nip22Comments.ReplyCommentPostScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
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.AccountSwitcherAndLeftDrawerLayout
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.BookmarkListScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.BookmarkListScreen
|
||||||
@@ -106,7 +106,6 @@ import java.net.URI
|
|||||||
fun AppNavigation(
|
fun AppNavigation(
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
accountStateViewModel: AccountStateViewModel,
|
accountStateViewModel: AccountStateViewModel,
|
||||||
sharedPreferencesViewModel: SharedPreferencesViewModel,
|
|
||||||
) {
|
) {
|
||||||
val nav = rememberNav()
|
val nav = rememberNav()
|
||||||
|
|
||||||
@@ -121,16 +120,16 @@ fun AppNavigation(
|
|||||||
composable<Route.Message> { MessagesScreen(accountViewModel, nav) }
|
composable<Route.Message> { MessagesScreen(accountViewModel, nav) }
|
||||||
composable<Route.Video> { VideoScreen(accountViewModel, nav) }
|
composable<Route.Video> { VideoScreen(accountViewModel, nav) }
|
||||||
composable<Route.Discover> { DiscoverScreen(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.EditProfile> { NewUserMetadataScreen(nav, accountViewModel) }
|
||||||
composable<Route.Search> { SearchScreen(accountViewModel, nav) }
|
composable<Route.Search> { SearchScreen(accountViewModel, nav) }
|
||||||
|
|
||||||
composableFromEnd<Route.SecurityFilters> { SecurityFiltersScreen(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.Bookmarks> { BookmarkListScreen(accountViewModel, nav) }
|
||||||
composableFromEnd<Route.Drafts> { DraftListScreen(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) }
|
composableFromEnd<Route.UserSettings> { UserSettingsScreen(accountViewModel, nav) }
|
||||||
composableFromBottomArgs<Route.Nip47NWCSetup> { NIP47SetupScreen(accountViewModel, nav, it.nip47) }
|
composableFromBottomArgs<Route.Nip47NWCSetup> { NIP47SetupScreen(accountViewModel, nav, it.nip47) }
|
||||||
composableFromEndArgs<Route.EditRelays> { AllRelayListScreen(accountViewModel, nav) }
|
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.AccountInfo
|
||||||
import com.vitorpamplona.amethyst.LocalPreferences
|
import com.vitorpamplona.amethyst.LocalPreferences
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||||
@@ -218,7 +217,7 @@ private fun AccountPicture(
|
|||||||
contentDescription = stringRes(R.string.profile_image),
|
contentDescription = stringRes(R.string.profile_image),
|
||||||
modifier = AccountPictureModifier,
|
modifier = AccountPictureModifier,
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
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.BuildConfig
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollowerCount
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollowerCount
|
||||||
@@ -247,7 +246,7 @@ fun ProfileContentTemplate(
|
|||||||
.border(3.dp, MaterialTheme.colorScheme.onBackground, CircleShape)
|
.border(3.dp, MaterialTheme.colorScheme.onBackground, CircleShape)
|
||||||
.clickable(onClick = onClick),
|
.clickable(onClick = onClick),
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (bestDisplayName != null) {
|
if (bestDisplayName != null) {
|
||||||
|
|||||||
+1
-2
@@ -32,7 +32,6 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
@@ -87,7 +86,7 @@ private fun LoggedInUserPictureDrawer(
|
|||||||
modifier = HeaderPictureModifier,
|
modifier = HeaderPictureModifier,
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
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 androidx.compose.ui.window.PopupProperties
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.NoteState
|
import com.vitorpamplona.amethyst.model.NoteState
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
@@ -605,7 +604,7 @@ fun WatchUserMetadataAndFollowsAndRenderUserProfilePicture(
|
|||||||
modifier = MaterialTheme.colorScheme.profile35dpModifier,
|
modifier = MaterialTheme.colorScheme.profile35dpModifier,
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
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.R
|
||||||
import com.vitorpamplona.amethyst.commons.compose.produceCachedStateAsync
|
import com.vitorpamplona.amethyst.commons.compose.produceCachedStateAsync
|
||||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
|
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelPicture
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelPicture
|
||||||
@@ -544,7 +543,7 @@ fun InnerNoteWithReactions(
|
|||||||
baseNote.event !is GenericRepostEvent &&
|
baseNote.event !is GenericRepostEvent &&
|
||||||
!isBoostedNote &&
|
!isBoostedNote &&
|
||||||
!isQuotedNote &&
|
!isQuotedNote &&
|
||||||
accountViewModel.settings.featureSet == FeatureSetType.COMPLETE
|
accountViewModel.settings.isCompleteUIMode()
|
||||||
NoteBody(
|
NoteBody(
|
||||||
baseNote = baseNote,
|
baseNote = baseNote,
|
||||||
showAuthorPicture = isQuotedNote,
|
showAuthorPicture = isQuotedNote,
|
||||||
@@ -1256,7 +1255,7 @@ fun BadgeBox(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
if (accountViewModel.settings.featureSet == FeatureSetType.COMPLETE) {
|
if (accountViewModel.settings.isCompleteUIMode()) {
|
||||||
if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) {
|
if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) {
|
||||||
baseNote.replyTo?.lastOrNull()?.let { RelayBadges(it, accountViewModel, nav) }
|
baseNote.replyTo?.lastOrNull()?.let { RelayBadges(it, accountViewModel, nav) }
|
||||||
} else {
|
} else {
|
||||||
@@ -1309,7 +1308,7 @@ private fun ChannelNotePicture(
|
|||||||
contentDescription = stringRes(R.string.group_picture),
|
contentDescription = stringRes(R.string.group_picture),
|
||||||
modifier = MaterialTheme.colorScheme.channelNotePictureModifier,
|
modifier = MaterialTheme.colorScheme.channelNotePictureModifier,
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
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.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||||
@@ -670,7 +669,7 @@ private fun SlidingAnimationCount(
|
|||||||
textColor: Color,
|
textColor: Color,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
) {
|
) {
|
||||||
if (accountViewModel.settings.featureSet == FeatureSetType.PERFORMANCE) {
|
if (accountViewModel.settings.isPerformanceMode()) {
|
||||||
TextCount(baseCount, textColor)
|
TextCount(baseCount, textColor)
|
||||||
} else {
|
} else {
|
||||||
AnimatedContent<Int>(
|
AnimatedContent<Int>(
|
||||||
@@ -719,7 +718,7 @@ fun SlidingAnimationAmount(
|
|||||||
textColor: Color,
|
textColor: Color,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
) {
|
) {
|
||||||
if (accountViewModel.settings.featureSet == FeatureSetType.PERFORMANCE) {
|
if (accountViewModel.settings.isPerformanceMode()) {
|
||||||
Text(
|
Text(
|
||||||
text = amount,
|
text = amount,
|
||||||
fontSize = Font14SP,
|
fontSize = Font14SP,
|
||||||
|
|||||||
@@ -50,14 +50,13 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
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.ClickableBox
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.LargeRelayIconModifier
|
import com.vitorpamplona.amethyst.ui.theme.LargeRelayIconModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
|
import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
|
||||||
@@ -126,7 +125,7 @@ fun RenderRelay(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val relayInfo by loadRelayInfo(relay, accountViewModel)
|
val relayInfo by loadRelayInfo(relay)
|
||||||
|
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
val clickableModifier =
|
val clickableModifier =
|
||||||
@@ -152,7 +151,7 @@ fun RenderRelay(
|
|||||||
iconUrl = relayInfo.icon,
|
iconUrl = relayInfo.icon,
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||||
pingInMs = 0,
|
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.compose.ui.unit.Dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||||
@@ -94,7 +93,7 @@ fun DisplayBlankAuthor(
|
|||||||
robot = "authornotfound",
|
robot = "authornotfound",
|
||||||
contentDescription = stringRes(R.string.unknown_author),
|
contentDescription = stringRes(R.string.unknown_author),
|
||||||
modifier = nullModifier,
|
modifier = nullModifier,
|
||||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +359,7 @@ fun InnerUserPicture(
|
|||||||
modifier = myImageModifier,
|
modifier = myImageModifier,
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
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) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
iMetaAttachments.downloadAndPrepare(item.link.url) {
|
iMetaAttachments.downloadAndPrepare(item.link.url) {
|
||||||
Amethyst.Companion.instance.okHttpClients
|
Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url)
|
||||||
.getHttpClient(
|
|
||||||
accountViewModel.account.privacyState.shouldUseTorForImageDownload(item.link.url),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
||||||
@@ -328,7 +327,7 @@ fun ShortCommunityHeader(
|
|||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = HeaderPictureModifier,
|
modifier = HeaderPictureModifier,
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,7 +377,7 @@ fun ShortCommunityHeaderNoActions(
|
|||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = HeaderPictureModifier,
|
modifier = HeaderPictureModifier,
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ fun VoiceHeader(
|
|||||||
callbackUri = callbackUri,
|
callbackUri = callbackUri,
|
||||||
mimeType = null,
|
mimeType = null,
|
||||||
aspectRatio = null,
|
aspectRatio = null,
|
||||||
proxyPort = accountViewModel.proxyPortForVideo(media),
|
proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(media),
|
||||||
keepPlaying = false,
|
keepPlaying = false,
|
||||||
waveformData = waveform,
|
waveformData = waveform,
|
||||||
) { mediaItem ->
|
) { mediaItem ->
|
||||||
|
|||||||
@@ -34,16 +34,17 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoggedInPage
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoggedInPage
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginOrSignupScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginOrSignupScreen
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AccountScreen(
|
fun AccountScreen(accountStateViewModel: AccountStateViewModel) {
|
||||||
accountStateViewModel: AccountStateViewModel,
|
// Pauses relay services when the app pauses
|
||||||
sharedPreferencesViewModel: SharedPreferencesViewModel,
|
ManageRelayServices()
|
||||||
) {
|
|
||||||
val accountState by accountStateViewModel.accountContent.collectAsStateWithLifecycle()
|
val accountState by accountStateViewModel.accountContent.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
Log.d("ActivityLifecycle", "AccountScreen $accountState $accountStateViewModel")
|
Log.d("ActivityLifecycle", "AccountScreen $accountState $accountStateViewModel")
|
||||||
@@ -55,11 +56,17 @@ fun AccountScreen(
|
|||||||
when (state) {
|
when (state) {
|
||||||
is AccountState.Loading -> LoadingSetup()
|
is AccountState.Loading -> LoadingSetup()
|
||||||
is AccountState.LoggedOff -> LoggedOffSetup(accountStateViewModel)
|
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
|
@Composable
|
||||||
fun LoadingSetup() {
|
fun LoadingSetup() {
|
||||||
// A surface container using the 'background' color from the theme
|
// A surface container using the 'background' color from the theme
|
||||||
@@ -91,14 +98,12 @@ fun LoggedOffSetup(accountStateViewModel: AccountStateViewModel) {
|
|||||||
fun LoggedInSetup(
|
fun LoggedInSetup(
|
||||||
state: AccountState.LoggedIn,
|
state: AccountState.LoggedIn,
|
||||||
accountStateViewModel: AccountStateViewModel,
|
accountStateViewModel: AccountStateViewModel,
|
||||||
sharedPreferencesViewModel: SharedPreferencesViewModel,
|
|
||||||
) {
|
) {
|
||||||
SetAccountCentricViewModelStore(state) {
|
SetAccountCentricViewModelStore(state) {
|
||||||
LoggedInPage(
|
LoggedInPage(
|
||||||
state.account,
|
account = state.account,
|
||||||
state.route,
|
route = state.route,
|
||||||
accountStateViewModel,
|
accountStateViewModel = accountStateViewModel,
|
||||||
sharedPreferencesViewModel,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-26
@@ -35,8 +35,6 @@ import com.vitorpamplona.amethyst.model.DefaultNIP65RelaySet
|
|||||||
import com.vitorpamplona.amethyst.model.DefaultSearchRelayList
|
import com.vitorpamplona.amethyst.model.DefaultSearchRelayList
|
||||||
import com.vitorpamplona.amethyst.service.Nip05NostrAddressVerifier
|
import com.vitorpamplona.amethyst.service.Nip05NostrAddressVerifier
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
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.hexToByteArray
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||||
@@ -108,7 +106,6 @@ class AccountStateViewModel : ViewModel() {
|
|||||||
|
|
||||||
suspend fun loginAndStartUI(
|
suspend fun loginAndStartUI(
|
||||||
key: String,
|
key: String,
|
||||||
torSettings: TorSettings,
|
|
||||||
transientAccount: Boolean,
|
transientAccount: Boolean,
|
||||||
loginWithExternalSigner: Boolean = false,
|
loginWithExternalSigner: Boolean = false,
|
||||||
packageName: String = "",
|
packageName: String = "",
|
||||||
@@ -142,31 +139,26 @@ class AccountStateViewModel : ViewModel() {
|
|||||||
keyPair = KeyPair(pubKey = pubKeyParsed),
|
keyPair = KeyPair(pubKey = pubKeyParsed),
|
||||||
transientAccount = transientAccount,
|
transientAccount = transientAccount,
|
||||||
externalSignerPackageName = packageName.ifBlank { "com.greenart7c3.nostrsigner" },
|
externalSignerPackageName = packageName.ifBlank { "com.greenart7c3.nostrsigner" },
|
||||||
torSettings = TorSettingsFlow.build(torSettings),
|
|
||||||
)
|
)
|
||||||
} else if (key.startsWith("nsec")) {
|
} else if (key.startsWith("nsec")) {
|
||||||
AccountSettings(
|
AccountSettings(
|
||||||
keyPair = KeyPair(privKey = key.bechToBytes()),
|
keyPair = KeyPair(privKey = key.bechToBytes()),
|
||||||
transientAccount = transientAccount,
|
transientAccount = transientAccount,
|
||||||
torSettings = TorSettingsFlow.build(torSettings),
|
|
||||||
)
|
)
|
||||||
} else if (key.contains(" ") && Nip06().isValidMnemonic(key)) {
|
} else if (key.contains(" ") && Nip06().isValidMnemonic(key)) {
|
||||||
AccountSettings(
|
AccountSettings(
|
||||||
keyPair = KeyPair(privKey = Nip06().privateKeyFromMnemonic(key)),
|
keyPair = KeyPair(privKey = Nip06().privateKeyFromMnemonic(key)),
|
||||||
transientAccount = transientAccount,
|
transientAccount = transientAccount,
|
||||||
torSettings = TorSettingsFlow.build(torSettings),
|
|
||||||
)
|
)
|
||||||
} else if (pubKeyParsed != null) {
|
} else if (pubKeyParsed != null) {
|
||||||
AccountSettings(
|
AccountSettings(
|
||||||
keyPair = KeyPair(pubKey = pubKeyParsed),
|
keyPair = KeyPair(pubKey = pubKeyParsed),
|
||||||
transientAccount = transientAccount,
|
transientAccount = transientAccount,
|
||||||
torSettings = TorSettingsFlow.build(torSettings),
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
AccountSettings(
|
AccountSettings(
|
||||||
keyPair = KeyPair(Hex.decode(key)),
|
keyPair = KeyPair(Hex.decode(key)),
|
||||||
transientAccount = transientAccount,
|
transientAccount = transientAccount,
|
||||||
torSettings = TorSettingsFlow.build(torSettings),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,7 +189,6 @@ class AccountStateViewModel : ViewModel() {
|
|||||||
fun login(
|
fun login(
|
||||||
key: String,
|
key: String,
|
||||||
password: String,
|
password: String,
|
||||||
torSettings: TorSettings,
|
|
||||||
transientAccount: Boolean,
|
transientAccount: Boolean,
|
||||||
loginWithExternalSigner: Boolean = false,
|
loginWithExternalSigner: Boolean = false,
|
||||||
packageName: String = "",
|
packageName: String = "",
|
||||||
@@ -222,41 +213,39 @@ class AccountStateViewModel : ViewModel() {
|
|||||||
onError("Could not decrypt key with provided password")
|
onError("Could not decrypt key with provided password")
|
||||||
Log.e("Login", "Could not decrypt ncryptsec")
|
Log.e("Login", "Could not decrypt ncryptsec")
|
||||||
} else {
|
} else {
|
||||||
loginSync(newKey, torSettings, transientAccount, loginWithExternalSigner, packageName, onError)
|
loginSync(newKey, transientAccount, loginWithExternalSigner, packageName, onError)
|
||||||
}
|
}
|
||||||
} else if (EMAIL_PATTERN.matcher(key).matches()) {
|
} else if (EMAIL_PATTERN.matcher(key).matches()) {
|
||||||
Nip05NostrAddressVerifier().verifyNip05(
|
Nip05NostrAddressVerifier().verifyNip05(
|
||||||
key,
|
key,
|
||||||
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(false) },
|
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(false) },
|
||||||
onSuccess = { publicKey ->
|
onSuccess = { publicKey ->
|
||||||
loginSync(Hex.decode(publicKey).toNpub(), torSettings, transientAccount, loginWithExternalSigner, packageName, onError)
|
loginSync(Hex.decode(publicKey).toNpub(), transientAccount, loginWithExternalSigner, packageName, onError)
|
||||||
},
|
},
|
||||||
onError = {
|
onError = {
|
||||||
onError(it)
|
onError(it)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
loginSync(key, torSettings, transientAccount, loginWithExternalSigner, packageName, onError)
|
loginSync(key, transientAccount, loginWithExternalSigner, packageName, onError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun login(
|
fun login(
|
||||||
key: String,
|
key: String,
|
||||||
torSettings: TorSettings,
|
|
||||||
transientAccount: Boolean,
|
transientAccount: Boolean,
|
||||||
loginWithExternalSigner: Boolean = false,
|
loginWithExternalSigner: Boolean = false,
|
||||||
packageName: String = "",
|
packageName: String = "",
|
||||||
onError: (String) -> Unit,
|
onError: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
loginSync(key, torSettings, transientAccount, loginWithExternalSigner, packageName, onError)
|
loginSync(key, transientAccount, loginWithExternalSigner, packageName, onError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun loginSync(
|
suspend fun loginSync(
|
||||||
key: String,
|
key: String,
|
||||||
torSettings: TorSettings,
|
|
||||||
transientAccount: Boolean,
|
transientAccount: Boolean,
|
||||||
loginWithExternalSigner: Boolean = false,
|
loginWithExternalSigner: Boolean = false,
|
||||||
packageName: String = "",
|
packageName: String = "",
|
||||||
@@ -266,7 +255,7 @@ class AccountStateViewModel : ViewModel() {
|
|||||||
if (_accountContent.value is AccountState.LoggedIn) {
|
if (_accountContent.value is AccountState.LoggedIn) {
|
||||||
prepareLogoutOrSwitch()
|
prepareLogoutOrSwitch()
|
||||||
}
|
}
|
||||||
loginAndStartUI(key, torSettings, transientAccount, loginWithExternalSigner, packageName)
|
loginAndStartUI(key, transientAccount, loginWithExternalSigner, packageName)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
Log.e("Login", "Could not sign in", e)
|
Log.e("Login", "Could not sign in", e)
|
||||||
@@ -274,10 +263,7 @@ class AccountStateViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newKey(
|
fun newKey(name: String? = null) {
|
||||||
torSettings: TorSettings,
|
|
||||||
name: String? = null,
|
|
||||||
) {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
if (_accountContent.value is AccountState.LoggedIn) {
|
if (_accountContent.value is AccountState.LoggedIn) {
|
||||||
prepareLogoutOrSwitch()
|
prepareLogoutOrSwitch()
|
||||||
@@ -285,7 +271,7 @@ class AccountStateViewModel : ViewModel() {
|
|||||||
|
|
||||||
_accountContent.update { AccountState.Loading }
|
_accountContent.update { AccountState.Loading }
|
||||||
|
|
||||||
val accountSettings = createNewAccount(torSettings, name)
|
val accountSettings = createNewAccount(name)
|
||||||
|
|
||||||
LocalPreferences.setDefaultAccount(accountSettings)
|
LocalPreferences.setDefaultAccount(accountSettings)
|
||||||
|
|
||||||
@@ -306,10 +292,7 @@ class AccountStateViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createNewAccount(
|
fun createNewAccount(name: String? = null): AccountSettings {
|
||||||
torSettings: TorSettings,
|
|
||||||
name: String? = null,
|
|
||||||
): AccountSettings {
|
|
||||||
val keyPair = KeyPair()
|
val keyPair = KeyPair()
|
||||||
val tempSigner = NostrSignerSync(keyPair)
|
val tempSigner = NostrSignerSync(keyPair)
|
||||||
|
|
||||||
@@ -327,7 +310,6 @@ class AccountStateViewModel : ViewModel() {
|
|||||||
backupDMRelayList = ChatMessageRelayListEvent.create(DefaultDMRelayList, tempSigner),
|
backupDMRelayList = ChatMessageRelayListEvent.create(DefaultDMRelayList, tempSigner),
|
||||||
backupSearchRelayList = SearchRelayListEvent.create(DefaultSearchRelayList.toList(), tempSigner),
|
backupSearchRelayList = SearchRelayListEvent.create(DefaultSearchRelayList.toList(), tempSigner),
|
||||||
backupChannelList = ChannelListEvent.create(emptyList(), DefaultChannels, 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.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import coil3.asDrawable
|
import coil3.asDrawable
|
||||||
import coil3.imageLoader
|
import coil3.imageLoader
|
||||||
import coil3.request.ImageRequest
|
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.Channel
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
||||||
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
|
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
|
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.nip51Lists.HiddenUsersState
|
||||||
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
|
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
|
||||||
import com.vitorpamplona.amethyst.model.observables.CreatedAtComparator
|
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.Nip05NostrAddressVerifier
|
||||||
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
|
|
||||||
import com.vitorpamplona.amethyst.service.Nip11Retriever
|
|
||||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||||
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
|
||||||
import com.vitorpamplona.amethyst.service.cashu.CashuToken
|
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.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
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.RelaySubscriptionsCoordinator
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.service.uploads.CompressorQuality
|
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.ZapraiserStatus
|
||||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||||
import com.vitorpamplona.amethyst.ui.note.showAmountInteger
|
import com.vitorpamplona.amethyst.ui.note.showAmountInteger
|
||||||
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
|
import com.vitorpamplona.amethyst.ui.screen.UiSettingsState
|
||||||
import com.vitorpamplona.amethyst.ui.screen.SharedSettingsState
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedState
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedState
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CombinedZap
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CombinedZap
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
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.ephemChat.chat.RoomId
|
||||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent
|
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent
|
||||||
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent
|
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.addressables.Address
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag
|
import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
|
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
|
import com.vitorpamplona.quartz.nip03Timestamp.DefaultOtsResolverBuilder
|
||||||
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
|
|
||||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
|
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
|
||||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||||
@@ -160,15 +158,15 @@ import kotlinx.coroutines.flow.onStart
|
|||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class AccountViewModel(
|
class AccountViewModel(
|
||||||
val account: Account,
|
val account: Account,
|
||||||
val settings: SharedSettingsState,
|
val settings: UiSettingsState,
|
||||||
|
val torSettings: TorSettingsFlow,
|
||||||
val dataSources: RelaySubscriptionsCoordinator,
|
val dataSources: RelaySubscriptionsCoordinator,
|
||||||
val okHttpClient: IHttpClientManager,
|
val httpClientBuilder: IRoleBasedHttpClientBuilder,
|
||||||
) : ViewModel(),
|
) : ViewModel(),
|
||||||
Dao {
|
Dao {
|
||||||
var firstRoute: Route? = null
|
var firstRoute: Route? = null
|
||||||
@@ -650,7 +648,7 @@ class AccountViewModel(
|
|||||||
message = message,
|
message = message,
|
||||||
context = context,
|
context = context,
|
||||||
showErrorIfNoLnAddress = showErrorIfNoLnAddress,
|
showErrorIfNoLnAddress = showErrorIfNoLnAddress,
|
||||||
okHttpClient = ::okHttpClientForMoney,
|
okHttpClient = httpClientBuilder::okHttpClientForMoney,
|
||||||
onError = onError,
|
onError = onError,
|
||||||
onProgress = onProgress,
|
onProgress = onProgress,
|
||||||
onPayViaIntent = onPayViaIntent,
|
onPayViaIntent = onPayViaIntent,
|
||||||
@@ -886,7 +884,7 @@ class AccountViewModel(
|
|||||||
onResult: suspend (UrlPreviewState) -> Unit,
|
onResult: suspend (UrlPreviewState) -> Unit,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
UrlCachedPreviewer.previewInfo(url, ::okHttpClientForPreview, onResult)
|
UrlCachedPreviewer.previewInfo(url, httpClientBuilder::okHttpClientForPreview, onResult)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -907,9 +905,7 @@ class AccountViewModel(
|
|||||||
Nip05NostrAddressVerifier()
|
Nip05NostrAddressVerifier()
|
||||||
.verifyNip05(
|
.verifyNip05(
|
||||||
nip05,
|
nip05,
|
||||||
okHttpClient = {
|
okHttpClient = httpClientBuilder::okHttpClientForNip05,
|
||||||
okHttpClient.getHttpClient(account.privacyState.shouldUseTorForNIP05(it))
|
|
||||||
},
|
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
// Marks user as verified
|
// Marks user as verified
|
||||||
if (it == pubkeyHex) {
|
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) {
|
fun runOnIO(runOnIO: () -> Unit) {
|
||||||
viewModelScope.launch(Dispatchers.IO) { runOnIO() }
|
viewModelScope.launch(Dispatchers.IO) { runOnIO() }
|
||||||
}
|
}
|
||||||
@@ -1073,7 +1054,7 @@ class AccountViewModel(
|
|||||||
|
|
||||||
suspend fun checkVideoIsOnline(videoUrl: String): Boolean =
|
suspend fun checkVideoIsOnline(videoUrl: String): Boolean =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
OnlineChecker.isOnline(videoUrl, ::okHttpClientForVideo)
|
OnlineChecker.isOnline(videoUrl, httpClientBuilder::okHttpClientForVideo)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadAndMarkAsRead(
|
fun loadAndMarkAsRead(
|
||||||
@@ -1108,19 +1089,19 @@ class AccountViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setTorSettings(newTorSettings: TorSettings) = runIOCatching { account.settings.setTorSettings(newTorSettings) }
|
|
||||||
|
|
||||||
class Factory(
|
class Factory(
|
||||||
val account: Account,
|
val account: Account,
|
||||||
val settings: SharedSettingsState,
|
val settings: UiSettingsState,
|
||||||
|
val torSettings: TorSettingsFlow,
|
||||||
val dataSources: RelaySubscriptionsCoordinator,
|
val dataSources: RelaySubscriptionsCoordinator,
|
||||||
val okHttpClient: IHttpClientManager,
|
val okHttpClient: RoleBasedHttpClientBuilder,
|
||||||
) : ViewModelProvider.Factory {
|
) : ViewModelProvider.Factory {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
||||||
AccountViewModel(
|
AccountViewModel(
|
||||||
account,
|
account,
|
||||||
settings,
|
settings,
|
||||||
|
torSettings,
|
||||||
dataSources,
|
dataSources,
|
||||||
okHttpClient,
|
okHttpClient,
|
||||||
) as T
|
) as T
|
||||||
@@ -1278,7 +1259,7 @@ class AccountViewModel(
|
|||||||
if (lud16 != null) {
|
if (lud16 != null) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val meltResult = MeltProcessor().melt(token, lud16, ::okHttpClientForMoney, context)
|
val meltResult = MeltProcessor().melt(token, lud16, httpClientBuilder::okHttpClientForMoney, context)
|
||||||
onDone(
|
onDone(
|
||||||
stringRes(context, R.string.cashu_successful_redemption),
|
stringRes(context, R.string.cashu_successful_redemption),
|
||||||
stringRes(
|
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
|
fun dataSources() = dataSources
|
||||||
|
|
||||||
suspend fun createTempDraftNote(noteEvent: DraftWrapEvent): Note? = draftNoteCache.update(noteEvent)
|
suspend fun createTempDraftNote(noteEvent: DraftWrapEvent): Note? = draftNoteCache.update(noteEvent)
|
||||||
@@ -1526,7 +1491,7 @@ class AccountViewModel(
|
|||||||
milliSats = milliSats,
|
milliSats = milliSats,
|
||||||
message = message,
|
message = message,
|
||||||
nostrRequest = zapRequest,
|
nostrRequest = zapRequest,
|
||||||
okHttpClient = ::okHttpClientForMoney,
|
okHttpClient = httpClientBuilder::okHttpClientForMoney,
|
||||||
onProgress = onProgress,
|
onProgress = onProgress,
|
||||||
context = context,
|
context = context,
|
||||||
)
|
)
|
||||||
@@ -1549,7 +1514,7 @@ class AccountViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
MediaSaverToDisk.saveDownloadingIfNeeded(
|
MediaSaverToDisk.saveDownloadingIfNeeded(
|
||||||
videoUri = videoUri,
|
videoUri = videoUri,
|
||||||
okHttpClient = ::okHttpClientForVideo,
|
okHttpClient = httpClientBuilder::okHttpClientForVideo,
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
localContext = localContext,
|
localContext = localContext,
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
@@ -1668,8 +1633,17 @@ var mockedCache: AccountViewModel? = null
|
|||||||
fun mockAccountViewModel(): AccountViewModel {
|
fun mockAccountViewModel(): AccountViewModel {
|
||||||
mockedCache?.let { return it }
|
mockedCache?.let { return it }
|
||||||
|
|
||||||
val sharedPreferencesViewModel: SharedPreferencesViewModel = viewModel()
|
val otsResolver = DefaultOtsResolverBuilder()
|
||||||
sharedPreferencesViewModel.init()
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val uiState =
|
||||||
|
UiSettingsState(
|
||||||
|
uiSettingsFlow = UiSettingsFlow(),
|
||||||
|
isMobileOrMeteredConnection = MutableStateFlow(false),
|
||||||
|
scope = scope,
|
||||||
|
)
|
||||||
|
|
||||||
val keyPair =
|
val keyPair =
|
||||||
KeyPair(
|
KeyPair(
|
||||||
privKey = Hex.decode("0f761f8a5a481e26f06605a1d9b3e9eba7a107d351f43c43a57469b788274499"),
|
privKey = Hex.decode("0f761f8a5a481e26f06605a1d9b3e9eba7a107d351f43c43a57469b788274499"),
|
||||||
@@ -1677,8 +1651,6 @@ fun mockAccountViewModel(): AccountViewModel {
|
|||||||
forceReplacePubkey = false,
|
forceReplacePubkey = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
val client = EmptyNostrClient
|
val client = EmptyNostrClient
|
||||||
|
|
||||||
val nwcFilters = NWCPaymentFilterAssembler(client)
|
val nwcFilters = NWCPaymentFilterAssembler(client)
|
||||||
@@ -1689,7 +1661,7 @@ fun mockAccountViewModel(): AccountViewModel {
|
|||||||
signer = NostrSignerInternal(keyPair),
|
signer = NostrSignerInternal(keyPair),
|
||||||
geolocationFlow = MutableStateFlow<LocationState.LocationResult>(LocationState.LocationResult.Loading),
|
geolocationFlow = MutableStateFlow<LocationState.LocationResult>(LocationState.LocationResult.Loading),
|
||||||
nwcFilterAssembler = nwcFilters,
|
nwcFilterAssembler = nwcFilters,
|
||||||
otsVerifCache = VerificationStateCache(),
|
otsResolverBuilder = otsResolver,
|
||||||
cache = LocalCache,
|
cache = LocalCache,
|
||||||
client = client,
|
client = client,
|
||||||
scope = scope,
|
scope = scope,
|
||||||
@@ -1697,8 +1669,9 @@ fun mockAccountViewModel(): AccountViewModel {
|
|||||||
|
|
||||||
return AccountViewModel(
|
return AccountViewModel(
|
||||||
account = account,
|
account = account,
|
||||||
settings = sharedPreferencesViewModel.sharedPrefs,
|
settings = uiState,
|
||||||
okHttpClient = EmptyHttpClientManager,
|
torSettings = TorSettingsFlow(torType = MutableStateFlow(TorType.OFF)),
|
||||||
|
httpClientBuilder = EmptyRoleBasedHttpClientBuilder(),
|
||||||
dataSources = RelaySubscriptionsCoordinator(LocalCache, client, scope),
|
dataSources = RelaySubscriptionsCoordinator(LocalCache, client, scope),
|
||||||
).also {
|
).also {
|
||||||
mockedCache = it
|
mockedCache = it
|
||||||
@@ -1712,8 +1685,17 @@ var vitorCache: AccountViewModel? = null
|
|||||||
fun mockVitorAccountViewModel(): AccountViewModel {
|
fun mockVitorAccountViewModel(): AccountViewModel {
|
||||||
mockedCache?.let { return it }
|
mockedCache?.let { return it }
|
||||||
|
|
||||||
val sharedPreferencesViewModel: SharedPreferencesViewModel = viewModel()
|
val scope = rememberCoroutineScope()
|
||||||
sharedPreferencesViewModel.init()
|
|
||||||
|
val otsResolver = DefaultOtsResolverBuilder()
|
||||||
|
|
||||||
|
val uiState =
|
||||||
|
UiSettingsState(
|
||||||
|
uiSettingsFlow = UiSettingsFlow(),
|
||||||
|
isMobileOrMeteredConnection = MutableStateFlow(false),
|
||||||
|
scope = scope,
|
||||||
|
)
|
||||||
|
|
||||||
val keyPair =
|
val keyPair =
|
||||||
KeyPair(
|
KeyPair(
|
||||||
pubKey = Hex.decode("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
pubKey = Hex.decode("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
|
||||||
@@ -1723,15 +1705,13 @@ fun mockVitorAccountViewModel(): AccountViewModel {
|
|||||||
|
|
||||||
val nwcFilters = NWCPaymentFilterAssembler(client)
|
val nwcFilters = NWCPaymentFilterAssembler(client)
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
val account =
|
val account =
|
||||||
Account(
|
Account(
|
||||||
settings = AccountSettings(keyPair),
|
settings = AccountSettings(keyPair),
|
||||||
signer = NostrSignerInternal(keyPair),
|
signer = NostrSignerInternal(keyPair),
|
||||||
geolocationFlow = MutableStateFlow<LocationState.LocationResult>(LocationState.LocationResult.Loading),
|
geolocationFlow = MutableStateFlow<LocationState.LocationResult>(LocationState.LocationResult.Loading),
|
||||||
nwcFilterAssembler = nwcFilters,
|
nwcFilterAssembler = nwcFilters,
|
||||||
otsVerifCache = VerificationStateCache(),
|
otsResolverBuilder = otsResolver,
|
||||||
cache = LocalCache,
|
cache = LocalCache,
|
||||||
client = EmptyNostrClient,
|
client = EmptyNostrClient,
|
||||||
scope = scope,
|
scope = scope,
|
||||||
@@ -1739,8 +1719,9 @@ fun mockVitorAccountViewModel(): AccountViewModel {
|
|||||||
|
|
||||||
return AccountViewModel(
|
return AccountViewModel(
|
||||||
account = account,
|
account = account,
|
||||||
settings = sharedPreferencesViewModel.sharedPrefs,
|
settings = uiState,
|
||||||
okHttpClient = EmptyHttpClientManager,
|
torSettings = TorSettingsFlow(torType = MutableStateFlow(TorType.OFF)),
|
||||||
|
httpClientBuilder = EmptyRoleBasedHttpClientBuilder(),
|
||||||
dataSources = RelaySubscriptionsCoordinator(LocalCache, client, scope),
|
dataSources = RelaySubscriptionsCoordinator(LocalCache, client, scope),
|
||||||
).also {
|
).also {
|
||||||
vitorCache = it
|
vitorCache = it
|
||||||
|
|||||||
+5
-38
@@ -30,7 +30,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
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.AppNavigation
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
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.chats.rooms.datasource.ChatroomListFilterAssemblerSubscription
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssemblerSubscription
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssemblerSubscription
|
||||||
@@ -62,7 +60,6 @@ fun LoggedInPage(
|
|||||||
account: Account,
|
account: Account,
|
||||||
route: Route?,
|
route: Route?,
|
||||||
accountStateViewModel: AccountStateViewModel,
|
accountStateViewModel: AccountStateViewModel,
|
||||||
sharedPreferencesViewModel: SharedPreferencesViewModel,
|
|
||||||
) {
|
) {
|
||||||
val accountViewModel: AccountViewModel =
|
val accountViewModel: AccountViewModel =
|
||||||
viewModel(
|
viewModel(
|
||||||
@@ -70,9 +67,10 @@ fun LoggedInPage(
|
|||||||
factory =
|
factory =
|
||||||
AccountViewModel.Factory(
|
AccountViewModel.Factory(
|
||||||
account = account,
|
account = account,
|
||||||
settings = sharedPreferencesViewModel.sharedPrefs,
|
settings = Amethyst.instance.uiState,
|
||||||
|
torSettings = Amethyst.instance.torPrefs.value,
|
||||||
dataSources = Amethyst.instance.sources,
|
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.
|
// Adds this account to the authentication procedures for relays.
|
||||||
RelayAuthSubscription(accountViewModel)
|
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.
|
// Loads account information + DMs and Notifications from Relays.
|
||||||
AccountFilterAssemblerSubscription(accountViewModel)
|
AccountFilterAssemblerSubscription(accountViewModel)
|
||||||
|
|
||||||
@@ -99,9 +91,6 @@ fun LoggedInPage(
|
|||||||
// Updates local cache of the anti-spam filter choice of this user.
|
// Updates local cache of the anti-spam filter choice of this user.
|
||||||
ObserveAntiSpamFilterSettings(accountViewModel)
|
ObserveAntiSpamFilterSettings(accountViewModel)
|
||||||
|
|
||||||
// Pauses relay services when the app pauses
|
|
||||||
ManageRelayServices(accountViewModel)
|
|
||||||
|
|
||||||
// Listens to Amber
|
// Listens to Amber
|
||||||
ListenToExternalSignerIfNeeded(accountViewModel)
|
ListenToExternalSignerIfNeeded(accountViewModel)
|
||||||
|
|
||||||
@@ -111,7 +100,6 @@ fun LoggedInPage(
|
|||||||
AppNavigation(
|
AppNavigation(
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
accountStateViewModel = accountStateViewModel,
|
accountStateViewModel = accountStateViewModel,
|
||||||
sharedPreferencesViewModel = sharedPreferencesViewModel,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,27 +111,6 @@ fun ObserveAntiSpamFilterSettings(accountViewModel: AccountViewModel) {
|
|||||||
Amethyst.instance.cache.antiSpam.active = isSpamActive
|
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)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun NotificationRegistration(accountViewModel: AccountViewModel) {
|
fun NotificationRegistration(accountViewModel: AccountViewModel) {
|
||||||
@@ -158,7 +125,7 @@ fun NotificationRegistration(accountViewModel: AccountViewModel) {
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
PushNotificationUtils.checkAndInit(
|
PushNotificationUtils.checkAndInit(
|
||||||
LocalPreferences.allSavedAccounts(),
|
LocalPreferences.allSavedAccounts(),
|
||||||
accountViewModel::okHttpClientForPushRegistration,
|
accountViewModel.httpClientBuilder::okHttpClientForPushRegistration,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +140,7 @@ fun NotificationRegistration(accountViewModel: AccountViewModel) {
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
PushNotificationUtils.checkAndInit(
|
PushNotificationUtils.checkAndInit(
|
||||||
LocalPreferences.allSavedAccounts(),
|
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.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
@@ -164,7 +163,7 @@ fun NormalChatNote(
|
|||||||
isLoggedInUser = isLoggedInUser,
|
isLoggedInUser = isLoggedInUser,
|
||||||
isDraft = note.event is DraftWrapEvent,
|
isDraft = note.event is DraftWrapEvent,
|
||||||
innerQuote = innerQuote,
|
innerQuote = innerQuote,
|
||||||
isComplete = accountViewModel.settings.featureSet == FeatureSetType.COMPLETE,
|
isComplete = accountViewModel.settings.isCompleteUIMode(),
|
||||||
hasDetailsToShow = note.zaps.isNotEmpty() || note.zapPayments.isNotEmpty() || note.reactions.isNotEmpty(),
|
hasDetailsToShow = note.zaps.isNotEmpty() || note.zapPayments.isNotEmpty() || note.reactions.isNotEmpty(),
|
||||||
drawAuthorInfo = drawAuthorInfo,
|
drawAuthorInfo = drawAuthorInfo,
|
||||||
parentBackgroundColor = parentBackgroundColor,
|
parentBackgroundColor = parentBackgroundColor,
|
||||||
|
|||||||
+4
-5
@@ -49,8 +49,8 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Constants
|
import com.vitorpamplona.amethyst.model.Constants
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
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.CreateTextWithEmoji
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
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.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.screen.loggedIn.mockAccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
|
import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
|
||||||
@@ -160,7 +159,7 @@ fun RenderChannelData(
|
|||||||
contentDescription = stringRes(R.string.channel_image),
|
contentDescription = stringRes(R.string.channel_image),
|
||||||
modifier = MaterialTheme.colorScheme.largeProfilePictureModifier,
|
modifier = MaterialTheme.colorScheme.largeProfilePictureModifier,
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,7 +236,7 @@ fun RenderRelayLinePublicChat(
|
|||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
@Suppress("ProduceStateDoesNotAssignValue")
|
@Suppress("ProduceStateDoesNotAssignValue")
|
||||||
val relayInfo by loadRelayInfo(relay, accountViewModel)
|
val relayInfo by loadRelayInfo(relay)
|
||||||
|
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
val clickableModifier =
|
val clickableModifier =
|
||||||
@@ -255,7 +254,7 @@ fun RenderRelayLinePublicChat(
|
|||||||
relayInfo.icon,
|
relayInfo.icon,
|
||||||
clickableModifier,
|
clickableModifier,
|
||||||
showPicture = accountViewModel.settings.showProfilePictures.value,
|
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) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
iMetaAttachments.downloadAndPrepare(item.link.url) {
|
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
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
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.foundation.layout.padding
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.State
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.produceState
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
|
||||||
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
|
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.channel.observeChannel
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
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.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
|
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
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
|
@Composable
|
||||||
fun ShortEphemeralChatChannelHeader(
|
fun ShortEphemeralChatChannelHeader(
|
||||||
@@ -119,16 +93,16 @@ private fun DrawRelayIcon(
|
|||||||
channel: EphemeralChatChannel,
|
channel: EphemeralChatChannel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
) {
|
) {
|
||||||
val relayInfo by loadRelayInfo(channel.roomId.relayUrl, accountViewModel)
|
val relayInfo by loadRelayInfo(channel.roomId.relayUrl)
|
||||||
|
|
||||||
RobohashFallbackAsyncImage(
|
RobohashFallbackAsyncImage(
|
||||||
robot = channel.roomId.toKey(),
|
robot = channel.roomId.toKey(),
|
||||||
model = relayInfo?.icon,
|
model = relayInfo.icon,
|
||||||
contentDescription = stringRes(R.string.profile_image),
|
contentDescription = stringRes(R.string.profile_image),
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = HeaderPictureModifier,
|
modifier = HeaderPictureModifier,
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
|
||||||
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
|
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel
|
||||||
@@ -89,7 +88,7 @@ fun LongPublicChatChannelHeader(
|
|||||||
contentDescription = stringRes(R.string.channel_image),
|
contentDescription = stringRes(R.string.channel_image),
|
||||||
modifier = MaterialTheme.colorScheme.largeProfilePictureModifier,
|
modifier = MaterialTheme.colorScheme.largeProfilePictureModifier,
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
|
||||||
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
|
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel
|
||||||
@@ -72,7 +71,7 @@ fun ShortPublicChatChannelHeader(
|
|||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = HeaderPictureModifier,
|
modifier = HeaderPictureModifier,
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
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,
|
alt = null,
|
||||||
sensitiveContent = null,
|
sensitiveContent = null,
|
||||||
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
|
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
|
||||||
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) },
|
okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads,
|
||||||
onProgress = {},
|
onProgress = {},
|
||||||
httpAuth = account::createHTTPAuthorization,
|
httpAuth = account::createHTTPAuthorization,
|
||||||
context = context,
|
context = context,
|
||||||
@@ -223,7 +223,7 @@ class ChannelMetadataViewModel : ViewModel() {
|
|||||||
alt = null,
|
alt = null,
|
||||||
sensitiveContent = null,
|
sensitiveContent = null,
|
||||||
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
|
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
|
||||||
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) },
|
okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads,
|
||||||
httpAuth = account::createBlossomUploadAuth,
|
httpAuth = account::createBlossomUploadAuth,
|
||||||
context = context,
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
|
||||||
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
|
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
|
||||||
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
|
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||||
@@ -74,7 +73,7 @@ fun ChannelFileUploadDialog(
|
|||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = HeaderPictureModifier,
|
modifier = HeaderPictureModifier,
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
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) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
iMetaAttachments.downloadAndPrepare(item.link.url) {
|
iMetaAttachments.downloadAndPrepare(item.link.url) {
|
||||||
Amethyst.instance.okHttpClients.getHttpClient(
|
Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url)
|
||||||
accountViewModel.account.privacyState.shouldUseTorForImageDownload(item.link.url),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-5
@@ -52,10 +52,10 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel
|
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.model.nip28PublicChats.PublicChatChannel
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteHasEvent
|
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.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.RoomNameDisplay
|
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.LoadEphemeralChatChannel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo
|
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.AccountPictureModifier
|
import com.vitorpamplona.amethyst.ui.theme.AccountPictureModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||||
@@ -197,7 +196,7 @@ private fun ChannelRoomCompose(
|
|||||||
channelLastContent = "$authorName: $description",
|
channelLastContent = "$authorName: $description",
|
||||||
hasNewMessages = (noteEvent?.createdAt ?: Long.MIN_VALUE) > lastReadTime,
|
hasNewMessages = (noteEvent?.createdAt ?: Long.MIN_VALUE) > lastReadTime,
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||||
onClick = { nav.nav(routeFor(channel)) },
|
onClick = { nav.nav(routeFor(channel)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -214,7 +213,7 @@ private fun ChannelRoomCompose(
|
|||||||
|
|
||||||
val channel = channelState?.channel as? EphemeralChatChannel ?: return
|
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 noteEvent = lastMessage.event
|
||||||
val description = noteEvent?.content?.take(200)
|
val description = noteEvent?.content?.take(200)
|
||||||
@@ -229,7 +228,7 @@ private fun ChannelRoomCompose(
|
|||||||
channelLastContent = "$authorName: $description",
|
channelLastContent = "$authorName: $description",
|
||||||
hasNewMessages = (noteEvent?.createdAt ?: Long.MIN_VALUE) > lastReadTime,
|
hasNewMessages = (noteEvent?.createdAt ?: Long.MIN_VALUE) > lastReadTime,
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||||
onClick = { nav.nav(routeFor(channel)) },
|
onClick = { nav.nav(routeFor(channel)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-4
@@ -541,10 +541,9 @@ open class NewProductViewModel :
|
|||||||
val wordToInsert = item.link.url + " "
|
val wordToInsert = item.link.url + " "
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
iMetaDescription.downloadAndPrepare(
|
iMetaDescription.downloadAndPrepare(item.link.url) {
|
||||||
item.link.url,
|
Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url)
|
||||||
{ Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.privacyState?.shouldUseTorForImageDownload(item.link.url) ?: false) },
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message = message.replaceCurrentWord(wordToInsert)
|
message = message.replaceCurrentWord(wordToInsert)
|
||||||
|
|||||||
+2
-6
@@ -815,12 +815,8 @@ open class ShortNotePostViewModel :
|
|||||||
val wordToInsert = item.link.url + " "
|
val wordToInsert = item.link.url + " "
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
iMetaAttachments.downloadAndPrepare(
|
iMetaAttachments.downloadAndPrepare(item.link.url) {
|
||||||
item.link.url,
|
Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url)
|
||||||
) {
|
|
||||||
Amethyst.instance.okHttpClients.getHttpClient(
|
|
||||||
accountViewModel.account.privacyState.shouldUseTorForImageDownload(item.link.url),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-35
@@ -20,9 +20,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications
|
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.Column
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -31,29 +28,24 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
||||||
import com.google.accompanist.permissions.PermissionState
|
|
||||||
import com.google.accompanist.permissions.isGranted
|
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.SelectNotificationProvider
|
import com.vitorpamplona.amethyst.ui.components.SelectNotificationProvider
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
|
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
|
||||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NotificationScreen(
|
fun NotificationScreen(
|
||||||
sharedPreferencesViewModel: SharedPreferencesViewModel,
|
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
NotificationScreen(
|
NotificationScreen(
|
||||||
notifFeedContentState = accountViewModel.feedStates.notifications,
|
notifFeedContentState = accountViewModel.feedStates.notifications,
|
||||||
notifSummaryState = accountViewModel.feedStates.notificationSummary,
|
notifSummaryState = accountViewModel.feedStates.notificationSummary,
|
||||||
sharedPreferencesViewModel = sharedPreferencesViewModel,
|
sharedPrefs = accountViewModel.settings.uiSettingsFlow,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
@@ -63,11 +55,11 @@ fun NotificationScreen(
|
|||||||
fun NotificationScreen(
|
fun NotificationScreen(
|
||||||
notifFeedContentState: CardFeedContentState,
|
notifFeedContentState: CardFeedContentState,
|
||||||
notifSummaryState: NotificationSummaryState,
|
notifSummaryState: NotificationSummaryState,
|
||||||
sharedPreferencesViewModel: SharedPreferencesViewModel,
|
sharedPrefs: UiSettingsFlow,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
SelectNotificationProvider(sharedPreferencesViewModel)
|
SelectNotificationProvider(sharedPrefs)
|
||||||
|
|
||||||
WatchAccountForNotifications(notifFeedContentState, accountViewModel)
|
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
|
@Composable
|
||||||
fun WatchAccountForNotifications(
|
fun WatchAccountForNotifications(
|
||||||
notifFeedContentState: CardFeedContentState,
|
notifFeedContentState: CardFeedContentState,
|
||||||
|
|||||||
+1
-1
@@ -585,7 +585,7 @@ class NewPublicMessageViewModel :
|
|||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
iMetaAttachments.downloadAndPrepare(item.link.url) {
|
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.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
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.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
|
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.PrivacySettingsBody
|
||||||
import com.vitorpamplona.amethyst.ui.tor.TorDialogViewModel
|
import com.vitorpamplona.amethyst.ui.tor.TorDialogViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||||
|
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PrivacyOptionsScreen(
|
fun PrivacyOptionsScreen(
|
||||||
accountViewModel: AccountViewModel,
|
torSettingsFlow: TorSettingsFlow,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val dialogViewModel = viewModel<TorDialogViewModel>()
|
val dialogViewModel = viewModel<TorDialogViewModel>()
|
||||||
|
|
||||||
LaunchedEffect(dialogViewModel, accountViewModel) {
|
// runs only once and before the rest of the screen is build
|
||||||
dialogViewModel.reset(
|
// to avoid blinking and animations from the default/previous
|
||||||
accountViewModel.account.settings.torSettings
|
// state to the current state
|
||||||
.toSettings(),
|
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(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
SavingTopBar(
|
SavingTopBar(
|
||||||
@@ -62,7 +77,7 @@ fun PrivacyOptionsScreen(
|
|||||||
nav.popBack()
|
nav.popBack()
|
||||||
},
|
},
|
||||||
onPost = {
|
onPost = {
|
||||||
accountViewModel.setTorSettings(dialogViewModel.save())
|
onPost(dialogViewModel.save())
|
||||||
nav.popBack()
|
nav.popBack()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -297,7 +297,7 @@ fun UrlVideoView(
|
|||||||
callbackUri = content.uri,
|
callbackUri = content.uri,
|
||||||
mimeType = content.mimeType,
|
mimeType = content.mimeType,
|
||||||
aspectRatio = ratio,
|
aspectRatio = ratio,
|
||||||
proxyPort = accountViewModel.proxyPortForVideo(content.url),
|
proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(content.url),
|
||||||
) { mediaItem ->
|
) { mediaItem ->
|
||||||
GetVideoController(
|
GetVideoController(
|
||||||
mediaItem = mediaItem,
|
mediaItem = mediaItem,
|
||||||
|
|||||||
+1
-1
@@ -89,7 +89,7 @@ private fun GalleryFeedLoaded(
|
|||||||
val items by loaded.feed.collectAsStateWithLifecycle()
|
val items by loaded.feed.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val ratio =
|
val ratio =
|
||||||
if (accountViewModel.settings.modernGalleryStyle.value) {
|
if (accountViewModel.settings.modernGalleryStyle()) {
|
||||||
0.8f
|
0.8f
|
||||||
} else {
|
} else {
|
||||||
1.0f
|
1.0f
|
||||||
|
|||||||
+2
-3
@@ -34,7 +34,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
@@ -194,7 +193,7 @@ private fun RenderBadgeImage(
|
|||||||
robot = "badgenotfound",
|
robot = "badgenotfound",
|
||||||
contentDescription = description,
|
contentDescription = description,
|
||||||
modifier = BadgePictureModifier,
|
modifier = BadgePictureModifier,
|
||||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
RobohashFallbackAsyncImage(
|
RobohashFallbackAsyncImage(
|
||||||
@@ -203,7 +202,7 @@ private fun RenderBadgeImage(
|
|||||||
contentDescription = description,
|
contentDescription = description,
|
||||||
modifier = BadgePictureModifier,
|
modifier = BadgePictureModifier,
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
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.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||||
import com.vitorpamplona.amethyst.ui.components.DisplayNIP05
|
import com.vitorpamplona.amethyst.ui.components.DisplayNIP05
|
||||||
@@ -166,7 +165,7 @@ fun ShowQRDialog(
|
|||||||
contentDescription = stringRes(R.string.profile_image),
|
contentDescription = stringRes(R.string.profile_image),
|
||||||
modifier = MaterialTheme.colorScheme.largeProfilePictureModifier,
|
modifier = MaterialTheme.colorScheme.largeProfilePictureModifier,
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Row(
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.vitorpamplona.amethyst.R
|
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.actions.CrossfadeIfEnabled
|
||||||
import com.vitorpamplona.amethyst.ui.components.ClickableEmail
|
import com.vitorpamplona.amethyst.ui.components.ClickableEmail
|
||||||
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
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.UserCompose
|
||||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.chats.rooms.LoadUser
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
@@ -126,7 +125,7 @@ fun RelayInformationScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { pad ->
|
) { pad ->
|
||||||
val relayInfo by loadRelayInfo(relay, accountViewModel)
|
val relayInfo by loadRelayInfo(relay)
|
||||||
|
|
||||||
val messages =
|
val messages =
|
||||||
remember(relay) {
|
remember(relay) {
|
||||||
@@ -158,7 +157,7 @@ fun RelayInformationScreen(
|
|||||||
displayUrl = relay.displayUrl(),
|
displayUrl = relay.displayUrl(),
|
||||||
iconUrl = relayInfo.icon,
|
iconUrl = relayInfo.icon,
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||||
RelayStats.get(relay).pingInMs,
|
RelayStats.get(relay).pingInMs,
|
||||||
iconModifier = LargeRelayIconModifier,
|
iconModifier = LargeRelayIconModifier,
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-2
@@ -37,11 +37,11 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import com.vitorpamplona.amethyst.R
|
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.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon
|
import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon
|
||||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
import com.vitorpamplona.amethyst.ui.theme.HalfHalfVertPadding
|
import com.vitorpamplona.amethyst.ui.theme.HalfHalfVertPadding
|
||||||
@@ -80,7 +80,7 @@ fun BasicRelaySetupInfoClickableRow(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = HalfVertPadding,
|
modifier = HalfVertPadding,
|
||||||
) {
|
) {
|
||||||
val iconUrlFromRelayInfoDoc by loadRelayInfo(item.relay, accountViewModel)
|
val iconUrlFromRelayInfoDoc by loadRelayInfo(item.relay)
|
||||||
|
|
||||||
RenderRelayIcon(
|
RenderRelayIcon(
|
||||||
iconUrlFromRelayInfoDoc.id ?: item.relay.displayUrl(),
|
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.Composable
|
||||||
import androidx.compose.runtime.getValue
|
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.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
@@ -37,7 +36,7 @@ fun BasicRelaySetupInfoDialog(
|
|||||||
BasicRelaySetupInfoClickableRow(
|
BasicRelaySetupInfoClickableRow(
|
||||||
item = item,
|
item = item,
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||||
onDelete = onDelete,
|
onDelete = onDelete,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
onClick = { nav.nav(Route.RelayInfo(item.relay.url)) },
|
onClick = { nav.nav(Route.RelayInfo(item.relay.url)) },
|
||||||
|
|||||||
+2
-6
@@ -24,7 +24,6 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
|
|
||||||
import com.vitorpamplona.amethyst.service.replace
|
import com.vitorpamplona.amethyst.service.replace
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
@@ -69,11 +68,8 @@ abstract class BasicRelaySetupInfoModel : ViewModel() {
|
|||||||
fun loadRelayDocuments() {
|
fun loadRelayDocuments() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
_relays.value.forEach { item ->
|
_relays.value.forEach { item ->
|
||||||
Nip11CachedRetriever.loadRelayInfo(
|
Amethyst.instance.nip11Cache.loadRelayInfo(
|
||||||
relay = item.relay,
|
relay = item.relay,
|
||||||
okHttpClient = {
|
|
||||||
Amethyst.instance.okHttpClients.getHttpClient(account.torRelayState.shouldUseTorForClean(item.relay))
|
|
||||||
},
|
|
||||||
onInfo = {
|
onInfo = {
|
||||||
togglePaidRelay(item, it.limitation?.payment_required ?: false)
|
togglePaidRelay(item, it.limitation?.payment_required ?: false)
|
||||||
},
|
},
|
||||||
@@ -91,7 +87,7 @@ abstract class BasicRelaySetupInfoModel : ViewModel() {
|
|||||||
relaySetupInfoBuilder(
|
relaySetupInfoBuilder(
|
||||||
normalized = it,
|
normalized = it,
|
||||||
forcesTor =
|
forcesTor =
|
||||||
account.torRelayState.flow.value
|
Amethyst.instance.torEvaluatorFlow.flow.value
|
||||||
.useTor(it),
|
.useTor(it),
|
||||||
)
|
)
|
||||||
}.distinctBy { it.relay }
|
}.distinctBy { it.relay }
|
||||||
|
|||||||
+2
-1
@@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
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.BasicRelaySetupInfo
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
|
||||||
@@ -36,7 +37,7 @@ class ConnectedRelayListViewModel : BasicRelaySetupInfoModel() {
|
|||||||
relay = it,
|
relay = it,
|
||||||
relayStat = RelayStats.get(it),
|
relayStat = RelayStats.get(it),
|
||||||
forcesTor =
|
forcesTor =
|
||||||
account.torRelayState.flow.value
|
Amethyst.instance.torEvaluatorFlow.flow.value
|
||||||
.useTor(it),
|
.useTor(it),
|
||||||
users =
|
users =
|
||||||
account.followsPerRelay.value[it]?.mapNotNull { hex ->
|
account.followsPerRelay.value[it]?.mapNotNull { hex ->
|
||||||
|
|||||||
+2
-5
@@ -25,7 +25,6 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
|
|
||||||
import com.vitorpamplona.amethyst.service.replace
|
import com.vitorpamplona.amethyst.service.replace
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
|
||||||
@@ -91,9 +90,8 @@ class Nip65RelayListViewModel : ViewModel() {
|
|||||||
fun loadRelayDocuments() {
|
fun loadRelayDocuments() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
_homeRelays.value.forEach { item ->
|
_homeRelays.value.forEach { item ->
|
||||||
Nip11CachedRetriever.loadRelayInfo(
|
Amethyst.instance.nip11Cache.loadRelayInfo(
|
||||||
relay = item.relay,
|
relay = item.relay,
|
||||||
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.torRelayState.shouldUseTorForClean(item.relay)) },
|
|
||||||
onInfo = {
|
onInfo = {
|
||||||
toggleHomePaidRelay(item, it.limitation?.payment_required ?: false)
|
toggleHomePaidRelay(item, it.limitation?.payment_required ?: false)
|
||||||
},
|
},
|
||||||
@@ -102,9 +100,8 @@ class Nip65RelayListViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_notificationRelays.value.forEach { item ->
|
_notificationRelays.value.forEach { item ->
|
||||||
Nip11CachedRetriever.loadRelayInfo(
|
Amethyst.instance.nip11Cache.loadRelayInfo(
|
||||||
relay = item.relay,
|
relay = item.relay,
|
||||||
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.torRelayState.shouldUseTorForClean(item.relay)) },
|
|
||||||
onInfo = {
|
onInfo = {
|
||||||
toggleNotifPaidRelay(item, it.limitation?.payment_required ?: false)
|
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.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
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.service.relayClient.searchCommand.TextSearchDataSourceSubscription
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
|
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
|
||||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
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.SearchIcon
|
||||||
import com.vitorpamplona.amethyst.ui.note.UserCompose
|
import com.vitorpamplona.amethyst.ui.note.UserCompose
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.screen.loggedIn.chats.rooms.ChannelName
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
@@ -294,7 +293,7 @@ private fun DisplaySearchResults(
|
|||||||
channelLastContent = item.summary(),
|
channelLastContent = item.summary(),
|
||||||
hasNewMessages = false,
|
hasNewMessages = false,
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||||
onClick = { nav.nav(routeFor(item)) },
|
onClick = { nav.nav(routeFor(item)) },
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -308,7 +307,7 @@ private fun DisplaySearchResults(
|
|||||||
ephemeralChannels,
|
ephemeralChannels,
|
||||||
key = { _, item -> "ephem" + item.roomId.toKey() },
|
key = { _, item -> "ephem" + item.roomId.toKey() },
|
||||||
) { _, item ->
|
) { _, item ->
|
||||||
val relayInfo by loadRelayInfo(item.roomId.relayUrl, accountViewModel)
|
val relayInfo by loadRelayInfo(item.roomId.relayUrl)
|
||||||
|
|
||||||
ChannelName(
|
ChannelName(
|
||||||
channelIdHex = item.roomId.toKey(),
|
channelIdHex = item.roomId.toKey(),
|
||||||
@@ -323,7 +322,7 @@ private fun DisplaySearchResults(
|
|||||||
channelLastContent = stringRes(R.string.ephemeral_relay_chat),
|
channelLastContent = stringRes(R.string.ephemeral_relay_chat),
|
||||||
hasNewMessages = false,
|
hasNewMessages = false,
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||||
onClick = { nav.nav(routeFor(item)) },
|
onClick = { nav.nav(routeFor(item)) },
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -350,7 +349,7 @@ private fun DisplaySearchResults(
|
|||||||
channelLastContent = item.summary(),
|
channelLastContent = item.summary(),
|
||||||
hasNewMessages = false,
|
hasNewMessages = false,
|
||||||
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
|
||||||
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
|
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||||
onClick = { nav.nav(routeFor(item)) },
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -35,6 +34,8 @@ import androidx.compose.foundation.verticalScroll
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.FeatureSetType
|
||||||
import com.vitorpamplona.amethyst.model.ProfileGalleryType
|
import com.vitorpamplona.amethyst.model.ProfileGalleryType
|
||||||
import com.vitorpamplona.amethyst.model.ThemeType
|
import com.vitorpamplona.amethyst.model.ThemeType
|
||||||
|
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
||||||
import com.vitorpamplona.amethyst.model.parseBooleanType
|
import com.vitorpamplona.amethyst.model.parseBooleanType
|
||||||
import com.vitorpamplona.amethyst.model.parseConnectivityType
|
import com.vitorpamplona.amethyst.model.parseConnectivityType
|
||||||
import com.vitorpamplona.amethyst.model.parseFeatureSetType
|
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.layouts.DisappearingScaffold
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
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.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.mockSharedPreferencesViewModel
|
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
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.Size10dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||||
@@ -78,6 +78,55 @@ import org.xmlpull.v1.XmlPullParser
|
|||||||
import org.xmlpull.v1.XmlPullParserException
|
import org.xmlpull.v1.XmlPullParserException
|
||||||
import java.io.IOException
|
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 {
|
fun Context.getLocaleListFromXml(): LocaleListCompat {
|
||||||
val tagsList = mutableListOf<CharSequence>()
|
val tagsList = mutableListOf<CharSequence>()
|
||||||
try {
|
try {
|
||||||
@@ -116,9 +165,8 @@ fun Context.getLangPreferenceDropdownEntries(): ImmutableMap<String, String> {
|
|||||||
|
|
||||||
fun getLanguageIndex(
|
fun getLanguageIndex(
|
||||||
languageEntries: ImmutableMap<String, String>,
|
languageEntries: ImmutableMap<String, String>,
|
||||||
sharedPreferencesViewModel: SharedPreferencesViewModel,
|
language: String?,
|
||||||
): Int {
|
): Int {
|
||||||
val language = sharedPreferencesViewModel.sharedPrefs.language
|
|
||||||
var languageIndex: Int
|
var languageIndex: Int
|
||||||
languageIndex =
|
languageIndex =
|
||||||
if (language != null) {
|
if (language != null) {
|
||||||
@@ -134,55 +182,153 @@ fun getLanguageIndex(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun ShowLanguageChoice(sharedPrefs: UiSettingsFlow) {
|
||||||
sharedPreferencesViewModel: SharedPreferencesViewModel,
|
val context = LocalContext.current
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
nav: INav,
|
val languageEntries = remember { context.getLangPreferenceDropdownEntries() }
|
||||||
) {
|
val languageList = remember { languageEntries.keys.map { TitleExplainer(it) }.toImmutableList() }
|
||||||
DisappearingScaffold(
|
|
||||||
isInvertedLayout = false,
|
val language by sharedPrefs.preferredLanguage.collectAsState()
|
||||||
topBar = {
|
|
||||||
TopBarWithBackButton(stringRes(id = R.string.application_preferences), nav::popBack)
|
val languageIndex = getLanguageIndex(languageEntries, language)
|
||||||
},
|
|
||||||
accountViewModel = accountViewModel,
|
SettingsRow(
|
||||||
|
R.string.language,
|
||||||
|
R.string.language_description,
|
||||||
|
languageList,
|
||||||
|
languageIndex,
|
||||||
) {
|
) {
|
||||||
Column(Modifier.padding(it)) {
|
sharedPrefs.preferredLanguage.tryEmit(languageEntries[languageList[it].title])
|
||||||
SettingsScreen(sharedPreferencesViewModel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(device = "spec:width=2160px,height=2340px,dpi=440")
|
|
||||||
@Composable
|
|
||||||
fun SettingsScreenPreview() {
|
|
||||||
val sharedPreferencesViewModel = mockSharedPreferencesViewModel()
|
|
||||||
ThemeComparisonRow {
|
|
||||||
SettingsScreen(sharedPreferencesViewModel)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(sharedPreferencesViewModel: SharedPreferencesViewModel) {
|
fun ShowThemeChoice(sharedPrefs: UiSettingsFlow) {
|
||||||
val selectedItens =
|
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(
|
persistentListOf(
|
||||||
TitleExplainer(stringRes(ConnectivityType.ALWAYS.resourceId)),
|
TitleExplainer(stringRes(ConnectivityType.ALWAYS.resourceId)),
|
||||||
TitleExplainer(stringRes(ConnectivityType.WIFI_ONLY.resourceId)),
|
TitleExplainer(stringRes(ConnectivityType.WIFI_ONLY.resourceId)),
|
||||||
TitleExplainer(stringRes(ConnectivityType.NEVER.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(
|
persistentListOf(
|
||||||
TitleExplainer(stringRes(ThemeType.SYSTEM.resourceId)),
|
TitleExplainer(stringRes(ConnectivityType.ALWAYS.resourceId)),
|
||||||
TitleExplainer(stringRes(ThemeType.LIGHT.resourceId)),
|
TitleExplainer(stringRes(ConnectivityType.WIFI_ONLY.resourceId)),
|
||||||
TitleExplainer(stringRes(ThemeType.DARK.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 =
|
val booleanItems =
|
||||||
persistentListOf(
|
persistentListOf(
|
||||||
TitleExplainer(stringRes(ConnectivityType.ALWAYS.resourceId)),
|
TitleExplainer(stringRes(ConnectivityType.ALWAYS.resourceId)),
|
||||||
TitleExplainer(stringRes(ConnectivityType.NEVER.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 =
|
val featureItems =
|
||||||
persistentListOf(
|
persistentListOf(
|
||||||
TitleExplainer(stringRes(FeatureSetType.COMPLETE.resourceId)),
|
TitleExplainer(stringRes(FeatureSetType.COMPLETE.resourceId)),
|
||||||
@@ -190,136 +336,35 @@ fun SettingsScreen(sharedPreferencesViewModel: SharedPreferencesViewModel) {
|
|||||||
TitleExplainer(stringRes(FeatureSetType.PERFORMANCE.resourceId)),
|
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 =
|
val galleryItems =
|
||||||
persistentListOf(
|
persistentListOf(
|
||||||
TitleExplainer(stringRes(ProfileGalleryType.CLASSIC.resourceId)),
|
TitleExplainer(stringRes(ProfileGalleryType.CLASSIC.resourceId)),
|
||||||
TitleExplainer(stringRes(ProfileGalleryType.MODERN.resourceId)),
|
TitleExplainer(stringRes(ProfileGalleryType.MODERN.resourceId)),
|
||||||
)
|
)
|
||||||
|
|
||||||
val showImagesIndex = sharedPreferencesViewModel.sharedPrefs.automaticallyShowImages.screenCode
|
val galleryIndex by sharedPrefs.gallerySet.collectAsState()
|
||||||
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 context = LocalContext.current
|
SettingsRow(
|
||||||
|
R.string.gallery_style,
|
||||||
val languageEntries = remember { context.getLangPreferenceDropdownEntries() }
|
R.string.gallery_style_description,
|
||||||
val languageList = remember { languageEntries.keys.map { TitleExplainer(it) }.toImmutableList() }
|
galleryItems,
|
||||||
val languageIndex = getLanguageIndex(languageEntries, sharedPreferencesViewModel)
|
galleryIndex.screenCode,
|
||||||
|
|
||||||
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(
|
sharedPrefs.gallerySet.tryEmit(parseGalleryType(it))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,15 +372,15 @@ fun SettingsScreen(sharedPreferencesViewModel: SharedPreferencesViewModel) {
|
|||||||
fun SettingsRow(
|
fun SettingsRow(
|
||||||
name: Int,
|
name: Int,
|
||||||
description: Int,
|
description: Int,
|
||||||
selectedItens: ImmutableList<TitleExplainer>,
|
selectedItems: ImmutableList<TitleExplainer>,
|
||||||
selectedIndex: Int,
|
selectedIndex: Int,
|
||||||
onSelect: (Int) -> Unit,
|
onSelect: (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
SettingsRow(name, description) {
|
SettingsRow(name, description) {
|
||||||
TextSpinner(
|
TextSpinner(
|
||||||
label = "",
|
label = "",
|
||||||
placeholder = selectedItens[selectedIndex].title,
|
placeholder = selectedItems[selectedIndex].title,
|
||||||
options = selectedItens,
|
options = selectedItems,
|
||||||
onSelect = onSelect,
|
onSelect = onSelect,
|
||||||
modifier = Modifier.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)),
|
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.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||||
@@ -327,7 +326,7 @@ private fun RenderAuthorInformation(
|
|||||||
NoteUsernameDisplay(note, Modifier.weight(1f), accountViewModel = accountViewModel)
|
NoteUsernameDisplay(note, Modifier.weight(1f), accountViewModel = accountViewModel)
|
||||||
VideoUserOptionAction(note, accountViewModel, nav)
|
VideoUserOptionAction(note, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
if (accountViewModel.settings.featureSet == FeatureSetType.COMPLETE) {
|
if (accountViewModel.settings.isCompleteUIMode()) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
ObserveDisplayNip05Status(
|
ObserveDisplayNip05Status(
|
||||||
note.author!!,
|
note.author!!,
|
||||||
|
|||||||
+5
-5
@@ -32,15 +32,15 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.amethyst.R
|
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.components.appendLink
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.tor.ConnectTorDialog
|
import com.vitorpamplona.amethyst.ui.tor.ConnectTorDialog
|
||||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TorSettingsSetup(
|
fun TorSettingsSetup(
|
||||||
torSettings: TorSettings,
|
torSettingsFlow: TorSettingsFlow,
|
||||||
onCheckedChange: (TorSettings) -> Unit,
|
|
||||||
onError: (String) -> Unit,
|
onError: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
var connectOrbotDialogOpen by remember { mutableStateOf(false) }
|
var connectOrbotDialogOpen by remember { mutableStateOf(false) }
|
||||||
@@ -57,11 +57,11 @@ fun TorSettingsSetup(
|
|||||||
|
|
||||||
if (connectOrbotDialogOpen) {
|
if (connectOrbotDialogOpen) {
|
||||||
ConnectTorDialog(
|
ConnectTorDialog(
|
||||||
torSettings = torSettings,
|
torSettings = torSettingsFlow.toSettings(),
|
||||||
onClose = { connectOrbotDialogOpen = false },
|
onClose = { connectOrbotDialogOpen = false },
|
||||||
onPost = { torSettings ->
|
onPost = { torSettings ->
|
||||||
connectOrbotDialogOpen = false
|
connectOrbotDialogOpen = false
|
||||||
onCheckedChange(torSettings)
|
torSettingsFlow.update(torSettings)
|
||||||
},
|
},
|
||||||
onError = onError,
|
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.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.commons.hashtags.Amethyst
|
import com.vitorpamplona.amethyst.commons.hashtags.Amethyst
|
||||||
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
|
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
|
||||||
@@ -170,15 +171,12 @@ fun LoginPage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
|
||||||
|
|
||||||
PasswordField(loginViewModel)
|
PasswordField(loginViewModel)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
TorSettingsSetup(
|
TorSettingsSetup(
|
||||||
torSettings = loginViewModel.torSettings,
|
torSettingsFlow = Amethyst.instance.torPrefs.value,
|
||||||
onCheckedChange = loginViewModel::updateTorSettings,
|
|
||||||
onError = {
|
onError = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
Toast
|
Toast
|
||||||
@@ -262,6 +260,8 @@ fun OfferTemporaryAccount(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun PasswordField(loginViewModel: LoginViewModel) {
|
private fun PasswordField(loginViewModel: LoginViewModel) {
|
||||||
if (loginViewModel.needsPassword) {
|
if (loginViewModel.needsPassword) {
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
val passwordFocusRequester = remember { FocusRequester() }
|
val passwordFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
PasswordField(
|
PasswordField(
|
||||||
|
|||||||
-9
@@ -28,7 +28,6 @@ import androidx.compose.ui.text.input.TextFieldValue
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
|
||||||
|
|
||||||
class LoginViewModel : ViewModel() {
|
class LoginViewModel : ViewModel() {
|
||||||
lateinit var accountStateViewModel: AccountStateViewModel
|
lateinit var accountStateViewModel: AccountStateViewModel
|
||||||
@@ -39,7 +38,6 @@ class LoginViewModel : ViewModel() {
|
|||||||
var acceptedTerms by mutableStateOf(false)
|
var acceptedTerms by mutableStateOf(false)
|
||||||
var termsAcceptanceIsRequiredError by mutableStateOf(false)
|
var termsAcceptanceIsRequiredError by mutableStateOf(false)
|
||||||
|
|
||||||
var torSettings by mutableStateOf(TorSettings())
|
|
||||||
var offerTemporaryLogin by mutableStateOf(false)
|
var offerTemporaryLogin by mutableStateOf(false)
|
||||||
var isTemporary by mutableStateOf(false)
|
var isTemporary by mutableStateOf(false)
|
||||||
|
|
||||||
@@ -77,7 +75,6 @@ class LoginViewModel : ViewModel() {
|
|||||||
processingLogin = false
|
processingLogin = false
|
||||||
isTemporary = false
|
isTemporary = false
|
||||||
offerTemporaryLogin = false
|
offerTemporaryLogin = false
|
||||||
torSettings = TorSettings()
|
|
||||||
isFirstLogin = false
|
isFirstLogin = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,10 +95,6 @@ class LoginViewModel : ViewModel() {
|
|||||||
errorManager.clearErrors()
|
errorManager.clearErrors()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateTorSettings(newTorSettings: TorSettings) {
|
|
||||||
torSettings = newTorSettings
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateAcceptedTerms(newAcceptedTerms: Boolean) {
|
fun updateAcceptedTerms(newAcceptedTerms: Boolean) {
|
||||||
acceptedTerms = newAcceptedTerms
|
acceptedTerms = newAcceptedTerms
|
||||||
if (newAcceptedTerms) {
|
if (newAcceptedTerms) {
|
||||||
@@ -140,7 +133,6 @@ class LoginViewModel : ViewModel() {
|
|||||||
accountStateViewModel.login(
|
accountStateViewModel.login(
|
||||||
key = key.text,
|
key = key.text,
|
||||||
password = password.text,
|
password = password.text,
|
||||||
torSettings = torSettings,
|
|
||||||
transientAccount = isTemporary,
|
transientAccount = isTemporary,
|
||||||
) {
|
) {
|
||||||
processingLogin = false
|
processingLogin = false
|
||||||
@@ -158,7 +150,6 @@ class LoginViewModel : ViewModel() {
|
|||||||
processingLogin = true
|
processingLogin = true
|
||||||
accountStateViewModel.login(
|
accountStateViewModel.login(
|
||||||
key = key.text,
|
key = key.text,
|
||||||
torSettings = torSettings,
|
|
||||||
transientAccount = isTemporary,
|
transientAccount = isTemporary,
|
||||||
loginWithExternalSigner = true,
|
loginWithExternalSigner = true,
|
||||||
packageName = packageName,
|
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.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.commons.hashtags.Amethyst
|
import com.vitorpamplona.amethyst.commons.hashtags.Amethyst
|
||||||
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
|
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
|
||||||
@@ -165,6 +166,20 @@ fun SignUpPage(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
TorSettingsSetup(
|
||||||
|
torSettingsFlow = Amethyst.instance.torPrefs.value,
|
||||||
|
onError = {
|
||||||
|
scope.launch {
|
||||||
|
Toast
|
||||||
|
.makeText(
|
||||||
|
context,
|
||||||
|
it,
|
||||||
|
Toast.LENGTH_LONG,
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
AcceptTerms(
|
AcceptTerms(
|
||||||
checked = signUpViewModel.acceptedTerms,
|
checked = signUpViewModel.acceptedTerms,
|
||||||
onCheckedChange = signUpViewModel::updateAcceptedTerms,
|
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))
|
Spacer(modifier = Modifier.height(Size10dp))
|
||||||
|
|
||||||
Box(modifier = Modifier.padding(Size40dp, 0.dp, Size40dp, 0.dp)) {
|
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.R
|
||||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedOff.login.LoginErrorManager
|
import com.vitorpamplona.amethyst.ui.screen.loggedOff.login.LoginErrorManager
|
||||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
|
||||||
|
|
||||||
class SignUpViewModel : ViewModel() {
|
class SignUpViewModel : ViewModel() {
|
||||||
lateinit var accountStateViewModel: AccountStateViewModel
|
lateinit var accountStateViewModel: AccountStateViewModel
|
||||||
@@ -40,8 +39,6 @@ class SignUpViewModel : ViewModel() {
|
|||||||
var acceptedTerms by mutableStateOf(false)
|
var acceptedTerms by mutableStateOf(false)
|
||||||
var termsAcceptanceIsRequiredError by mutableStateOf(false)
|
var termsAcceptanceIsRequiredError by mutableStateOf(false)
|
||||||
|
|
||||||
var torSettings by mutableStateOf(TorSettings())
|
|
||||||
|
|
||||||
fun init(accountStateViewModel: AccountStateViewModel) {
|
fun init(accountStateViewModel: AccountStateViewModel) {
|
||||||
this.accountStateViewModel = accountStateViewModel
|
this.accountStateViewModel = accountStateViewModel
|
||||||
}
|
}
|
||||||
@@ -51,10 +48,6 @@ class SignUpViewModel : ViewModel() {
|
|||||||
errorManager.clearErrors()
|
errorManager.clearErrors()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateTorSettings(newTorSettings: TorSettings) {
|
|
||||||
torSettings = newTorSettings
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateAcceptedTerms(newAcceptedTerms: Boolean) {
|
fun updateAcceptedTerms(newAcceptedTerms: Boolean) {
|
||||||
acceptedTerms = newAcceptedTerms
|
acceptedTerms = newAcceptedTerms
|
||||||
if (newAcceptedTerms) {
|
if (newAcceptedTerms) {
|
||||||
@@ -80,7 +73,7 @@ class SignUpViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun signup() {
|
fun signup() {
|
||||||
if (checkCanSignup()) {
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.halilibo.richtext.ui.RichTextStyle
|
import com.halilibo.richtext.ui.RichTextStyle
|
||||||
import com.halilibo.richtext.ui.resolveDefaults
|
import com.halilibo.richtext.ui.resolveDefaults
|
||||||
import com.patrykandpatrick.vico.compose.common.VicoTheme
|
import com.patrykandpatrick.vico.compose.common.VicoTheme
|
||||||
import com.patrykandpatrick.vico.compose.common.VicoTheme.CandlestickCartesianLayerColors
|
import com.patrykandpatrick.vico.compose.common.VicoTheme.CandlestickCartesianLayerColors
|
||||||
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.model.ThemeType
|
import com.vitorpamplona.amethyst.model.ThemeType
|
||||||
|
|
||||||
private val DarkColorPalette =
|
private val DarkColorPalette =
|
||||||
@@ -482,6 +484,14 @@ val chartDarkColors =
|
|||||||
val ColorScheme.chartStyle: VicoTheme
|
val ColorScheme.chartStyle: VicoTheme
|
||||||
get() = if (isLight) chartLightColors else chartDarkColors
|
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
|
@Composable
|
||||||
fun AmethystTheme(
|
fun AmethystTheme(
|
||||||
prefTheme: ThemeType,
|
prefTheme: ThemeType,
|
||||||
|
|||||||
@@ -20,12 +20,19 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.tor
|
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.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
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.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.transformLatest
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* There should be only one instance of the Tor binding per app.
|
* 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.
|
* Tor will connect as soon as status is listened to.
|
||||||
*/
|
*/
|
||||||
class TorManager(
|
class TorManager(
|
||||||
app: Application,
|
torPrefs: TorSharedPreferences,
|
||||||
|
app: Context,
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
) {
|
) {
|
||||||
val status: StateFlow<TorServiceStatus> =
|
val service = TorService(app)
|
||||||
TorService(app).status.stateIn(
|
|
||||||
scope,
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
SharingStarted.WhileSubscribed(30000),
|
val status =
|
||||||
TorServiceStatus.Off,
|
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?> =
|
val activePortOrNull: StateFlow<Int?> =
|
||||||
status
|
status
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.tor
|
|||||||
|
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
|
||||||
class TorSettings(
|
data class TorSettings(
|
||||||
val torType: TorType = TorType.INTERNAL,
|
val torType: TorType = TorType.INTERNAL,
|
||||||
val externalSocksPort: Int = 9050,
|
val externalSocksPort: Int = 9050,
|
||||||
val onionRelaysViaTor: Boolean = true,
|
val onionRelaysViaTor: Boolean = true,
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ class TorSettingsFlow(
|
|||||||
fun update(torSettings: TorSettings): Boolean {
|
fun update(torSettings: TorSettings): Boolean {
|
||||||
var any = false
|
var any = false
|
||||||
|
|
||||||
|
println("AABBCC updating 1 $torSettings")
|
||||||
|
|
||||||
if (torType.value != torSettings.torType) {
|
if (torType.value != torSettings.torType) {
|
||||||
torType.tryEmit(torSettings.torType)
|
torType.tryEmit(torSettings.torType)
|
||||||
any = true
|
any = true
|
||||||
@@ -148,6 +150,8 @@ class TorSettingsFlow(
|
|||||||
any = true
|
any = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
println("AABBCC updating 2 ${toSettings()}")
|
||||||
|
|
||||||
return any
|
return any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-5
@@ -20,19 +20,36 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.components
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.runtime.Composable
|
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.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.checkifItNeedsToRequestNotificationPermission
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
|
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesViewModel) {
|
fun SelectNotificationProvider(sharedPrefs: UiSettingsFlow) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
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
|
@Composable
|
||||||
fun PushNotificationSettingsRow(sharedPreferencesViewModel: SharedPreferencesViewModel) {}
|
fun PushNotificationSettingsRow(sharedPrefs: UiSettingsFlow) {}
|
||||||
|
|||||||
+1
-1
@@ -459,7 +459,7 @@ open class BasicRelayClient(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
socket?.let {
|
socket?.let {
|
||||||
// Log.d(logTag, "Sending: $str")
|
Log.d(logTag, "Sending: $str")
|
||||||
val result = it.send(str)
|
val result = it.send(str)
|
||||||
listener.onSend(this@BasicRelayClient, str, result)
|
listener.onSend(this@BasicRelayClient, str, result)
|
||||||
stats.addBytesSent(str.bytesUsedInMemory())
|
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.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
class VerificationStateCache {
|
class VerificationStateCache(
|
||||||
|
val otsResolverBuilder: OtsResolverBuilder,
|
||||||
|
) {
|
||||||
private val cache = LruCache<HexKey, VerificationState>(200)
|
private val cache = LruCache<HexKey, VerificationState>(200)
|
||||||
|
|
||||||
fun verify(
|
fun verify(event: OtsEvent): VerificationState {
|
||||||
event: OtsEvent,
|
|
||||||
resolverBuilder: OtsResolverBuilder,
|
|
||||||
): VerificationState {
|
|
||||||
cache.put(event.id, VerificationState.Verifying)
|
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(
|
fun cacheVerify(event: OtsEvent): VerificationState =
|
||||||
event: OtsEvent,
|
|
||||||
resolverBuilder: OtsResolverBuilder,
|
|
||||||
): VerificationState =
|
|
||||||
when (val verif = cache[event.id]) {
|
when (val verif = cache[event.id]) {
|
||||||
is VerificationState.Verifying -> verif
|
is VerificationState.Verifying -> verif
|
||||||
is VerificationState.Verified -> verif
|
is VerificationState.Verified -> verif
|
||||||
is VerificationState.NetworkError -> {
|
is VerificationState.NetworkError -> {
|
||||||
// try again in 5 mins
|
// try again in 5 mins
|
||||||
if (verif.time < TimeUtils.fiveMinutesAgo()) {
|
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 {
|
} else {
|
||||||
verify(event, resolverBuilder)
|
verify(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is VerificationState.Error -> verif
|
is VerificationState.Error -> verif
|
||||||
else -> {
|
else -> {
|
||||||
verify(event, resolverBuilder)
|
verify(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user