diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 06f2af1f2..04a82a0c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -57,6 +57,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext @@ -74,7 +75,7 @@ data class AccountInfo( val npub: String, val hasPrivKey: Boolean, val loggedInWithExternalSigner: Boolean, - val isTransient: Boolean = false, + val isTransient: Boolean, ) private object PrefKeys { @@ -126,7 +127,7 @@ object LocalPreferences { private const val COMMA = "," private var currentAccount: String? = null - private var savedAccounts: List? = null + private var savedAccounts: MutableStateFlow?> = MutableStateFlow(null) private var cachedAccounts: MutableMap = mutableMapOf() suspend fun currentAccount(): String? { @@ -156,7 +157,7 @@ object LocalPreferences { } private suspend fun savedAccounts(): List { - if (savedAccounts == null) { + if (savedAccounts.value == null) { withContext(Dispatchers.IO) { with(encryptedPreferences()) { val newSystemOfAccounts = @@ -165,7 +166,7 @@ object LocalPreferences { } if (!newSystemOfAccounts.isNullOrEmpty()) { - savedAccounts = newSystemOfAccounts + savedAccounts.emit(newSystemOfAccounts) } else { val oldAccounts = getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(COMMA) ?: listOf() @@ -174,27 +175,28 @@ object LocalPreferences { AccountInfo( npub, encryptedPreferences(npub).getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false), - (encryptedPreferences(npub).getString(PrefKeys.NOSTR_PRIVKEY, "") ?: "") - .isNotBlank(), + (encryptedPreferences(npub).getString(PrefKeys.NOSTR_PRIVKEY, "") ?: "").isNotBlank(), + false, ) } - savedAccounts = migrated + savedAccounts.emit(migrated) edit().apply { putString(PrefKeys.ALL_ACCOUNT_INFO, Event.mapper.writeValueAsString(savedAccounts)) }.apply() } } } } - return savedAccounts!! + // it's always not null when it gets here. + return savedAccounts.value!! } - fun cachedAccounts() = savedAccounts + fun accountsFlow() = savedAccounts private suspend fun updateSavedAccounts(accounts: List) = withContext(Dispatchers.IO) { if (savedAccounts != accounts) { - savedAccounts = accounts + savedAccounts.emit(accounts) encryptedPreferences() .edit() @@ -269,7 +271,7 @@ object LocalPreferences { */ @SuppressLint("ApplySharedPref") suspend fun updatePrefsForLogout(accountInfo: AccountInfo) { - Log.d("LocalPreferences", "Saving to encrypted storage updatePrefsForLogout") + Log.d("LocalPreferences", "Saving to encrypted storage updatePrefsForLogout ${accountInfo.npub}") withContext(Dispatchers.IO) { encryptedPreferences(accountInfo.npub).edit().clear().commit() removeAccount(accountInfo) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 76240732d..3c6daa69f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -324,7 +324,7 @@ fun uriToRoute(uri: String?): String? = uri?.let { Nip47WalletConnect.parse(it) val encodedUri = URLEncoder.encode(it, StandardCharsets.UTF_8.toString()) - Route.Home.base + "?nip47=" + encodedUri + Route.NIP47Setup.base + "?nip47=" + encodedUri } } catch (e: Exception) { if (e is CancellationException) throw e diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index 457d072b0..2911754e8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -161,9 +161,9 @@ fun ZoomableContentView( nostrUriCallback = content.uri, onDialog = { dialogOpen = true - if (!isFoldableOrLarge && !isOrientationLocked) { - DeviceUtils.changeDeviceOrientation(isLandscapeMode, activity) - } + // if (!isFoldableOrLarge && !isOrientationLocked) { + // DeviceUtils.changeDeviceOrientation(isLandscapeMode, activity) + // } }, accountViewModel = accountViewModel, ) @@ -201,7 +201,7 @@ fun ZoomableContentView( images, onDismiss = { dialogOpen = false - if (!isFoldableOrLarge && !isOrientationLocked) DeviceUtils.changeDeviceOrientation(isLandscapeMode, activity) + // if (!isFoldableOrLarge && !isOrientationLocked) DeviceUtils.changeDeviceOrientation(isLandscapeMode, activity) }, accountViewModel, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/RememberForeverStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/RememberForeverStates.kt index db08e6770..a164733ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/RememberForeverStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/RememberForeverStates.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.feeds -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.pager.PagerState @@ -115,20 +114,35 @@ fun rememberForeverLazyListState( return scrollState } -@OptIn(ExperimentalFoundationApi::class) @Composable fun rememberForeverPagerState( key: String, initialFirstVisibleItemIndex: Int = 0, initialFirstVisibleItemScrollOffset: Float = 0.0f, pageCount: () -> Int, +): PagerState = + rememberForeverPagerState(key, initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset, pageCount) { initialPage, initialPageOffsetFraction, pageCount -> + rememberPagerState(initialPage, initialPageOffsetFraction, pageCount) + } + +@Composable +fun rememberForeverPagerState( + key: String, + initialFirstVisibleItemIndex: Int = 0, + initialFirstVisibleItemScrollOffset: Float = 0.0f, + pageCount: () -> Int, + rememberPagerStateFunction: @Composable ( + initialPage: Int, + initialPageOffsetFraction: Float, + pageCount: () -> Int, + ) -> PagerState, ): PagerState { val savedValue = savedScrollStates[key] val savedIndex = savedValue?.index ?: initialFirstVisibleItemIndex val savedOffset = savedValue?.scrollOffsetFraction ?: initialFirstVisibleItemScrollOffset val scrollState = - rememberPagerState( + rememberPagerStateFunction( savedIndex, savedOffset, pageCount, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt index fdf671472..6aeb227f2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt @@ -47,7 +47,6 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -55,6 +54,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R @@ -73,6 +73,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.forEach import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -81,14 +82,6 @@ fun AccountSwitchBottomSheet( accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel, ) { - @Suppress("ProduceStateDoesNotAssignValue") - val accounts by - produceState(initialValue = LocalPreferences.cachedAccounts()) { - if (value == null) { - value = LocalPreferences.allSavedAccounts() - } - } - var popupExpanded by remember { mutableStateOf(false) } val scrollState = rememberScrollState() @@ -103,7 +96,7 @@ fun AccountSwitchBottomSheet( ) { Text(stringRes(R.string.account_switch_select_account), fontWeight = FontWeight.Bold) } - accounts?.forEach { acc -> DisplayAccount(acc, accountViewModel, accountStateViewModel) } + DisplayAllAccounts(accountViewModel, accountStateViewModel) Row( modifier = Modifier @@ -123,13 +116,22 @@ fun AccountSwitchBottomSheet( } } +@Composable +private fun DisplayAllAccounts( + accountViewModel: AccountViewModel, + accountStateViewModel: AccountStateViewModel, +) { + val accounts by LocalPreferences.accountsFlow().collectAsStateWithLifecycle() + accounts?.forEach { acc -> DisplayAccount(acc, accountViewModel, accountStateViewModel) } +} + @Composable fun DisplayAccount( acc: AccountInfo, accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel, ) { - var baseUser by remember { + var baseUser by remember(acc) { mutableStateOf( decodePublicKeyAsHexOrNull(acc.npub)?.let { LocalCache.getUserIfExists(it) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index cb83f3698..b90e526bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -67,6 +67,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.HomeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ProfileScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NIP47SetupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SecurityFiltersScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.ThreadScreen @@ -100,24 +101,7 @@ fun AppNavigation( enterTransition = { fadeIn(animationSpec = tween(200)) }, exitTransition = { fadeOut(animationSpec = tween(200)) }, ) { - composable( - Route.Home.route, - Route.Home.arguments, - ) { - val nip47 = it.arguments?.getString("nip47") - - HomeScreen(accountViewModel, nav, nip47) - - if (nip47 != null) { - LaunchedEffect(key1 = Unit) { - launch { - delay(1000) - it.arguments?.remove("nip47") - } - } - } - } - + composable(Route.Home.route) { HomeScreen(accountViewModel, nav) } composable(Route.Message.route) { ChatroomListScreen(accountViewModel, nav) } composable(Route.Video.route) { VideoScreen(accountViewModel, nav) } composable(Route.Discover.route) { DiscoverScreen(accountViewModel, nav) } @@ -282,6 +266,19 @@ fun AppNavigation( nav, ) } + + composable( + Route.NIP47Setup.route, + Route.NIP47Setup.arguments, + enterTransition = { slideInHorizontallyFromEnd }, + exitTransition = { scaleOut }, + popEnterTransition = { scaleIn }, + popExitTransition = { slideOutHorizontallyToEnd }, + ) { + val nip47 = it.arguments?.getString("nip47") + + NIP47SetupScreen(accountViewModel, nav, nip47) + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index f30e4195d..6a35447cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -52,18 +52,10 @@ sealed class Route( ) { object Home : Route( - route = "Home?nip47={nip47}", + route = "Home", icon = R.drawable.ic_home, notifSize = Modifier.size(Size25dp), iconSize = Modifier.size(Size24dp), - arguments = - listOf( - navArgument("nip47") { - type = NavType.StringType - nullable = true - defaultValue = null - }, - ).toImmutableList(), contentDescriptor = R.string.route_home, ) @@ -92,9 +84,6 @@ sealed class Route( Route( route = "Discover", icon = R.drawable.ic_sensors, - // hasNewItems = { accountViewModel, newNotes -> - // DiscoverLatestItem.hasNewItems(accountViewModel, newNotes) - // }, contentDescriptor = R.string.route_discover, ) @@ -217,6 +206,20 @@ sealed class Route( route = "Settings", icon = R.drawable.ic_settings, ) + + object NIP47Setup : + Route( + route = "NIP47Setup?nip47={nip47}", + icon = R.drawable.ic_home, + arguments = + listOf( + navArgument("nip47") { + type = NavType.StringType + nullable = true + defaultValue = null + }, + ).toImmutableList(), + ) } fun getRouteWithArguments(navController: NavHostController): String? { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index a36f40f91..0f0414af1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -31,7 +31,6 @@ import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt -import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi @@ -220,16 +219,66 @@ fun UpdateZapAmountDialog( onClose: () -> Unit, nip47uri: String? = null, accountViewModel: AccountViewModel, +) { + Dialog( + onDismissRequest = { onClose() }, + properties = + DialogProperties( + usePlatformDefaultWidth = false, + dismissOnClickOutside = false, + decorFitsSystemWindows = false, + ), + ) { + Surface( + modifier = Modifier.fillMaxWidth(), + ) { + Column { + val postViewModel: UpdateZapAmountViewModel = + viewModel( + key = "UpdateZapAmountViewModel", + factory = UpdateZapAmountViewModel.Factory(accountViewModel.account.settings), + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + CloseButton( + onPress = { + postViewModel.cancel() + onClose() + }, + ) + + SaveButton( + onPost = { + postViewModel.sendPost() + onClose() + }, + isActive = postViewModel.hasChanged(), + ) + } + + Spacer(modifier = Modifier.height(10.dp)) + + UpdateZapAmountContent(postViewModel, onClose, nip47uri, accountViewModel) + } + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun UpdateZapAmountContent( + postViewModel: UpdateZapAmountViewModel, + onClose: () -> Unit, + nip47uri: String? = null, + accountViewModel: AccountViewModel, ) { val context = LocalContext.current val clipboardManager = LocalClipboardManager.current - val postViewModel: UpdateZapAmountViewModel = - viewModel( - key = "UpdateZapAmountViewModel", - factory = UpdateZapAmountViewModel.Factory(accountViewModel.account.settings), - ) - val uri = LocalUriHandler.current val zapTypes = @@ -282,337 +331,295 @@ fun UpdateZapAmountDialog( } } - Dialog( - onDismissRequest = { onClose() }, - properties = - DialogProperties( - usePlatformDefaultWidth = false, - dismissOnClickOutside = false, - decorFitsSystemWindows = false, - ), + Column( + modifier = + Modifier + .padding(10.dp) + .fillMaxWidth() + .imePadding() + .verticalScroll(rememberScrollState()), ) { - Surface( + FlowRow( modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, ) { - Column(modifier = Modifier.padding(10.dp).imePadding()) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, + postViewModel.amountSet.forEach { amountInSats -> + Button( + modifier = Modifier.padding(horizontal = 3.dp), + shape = ButtonBorder, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), + onClick = { postViewModel.removeAmount(amountInSats) }, ) { - CloseButton( - onPress = { - postViewModel.cancel() - onClose() - }, - ) - - SaveButton( - onPost = { - postViewModel.sendPost() - onClose() - }, - isActive = postViewModel.hasChanged(), + Text( + "⚡ ${ + showAmount( + amountInSats.toBigDecimal().setScale(1), + ) + } ✖", + color = Color.White, + textAlign = TextAlign.Center, ) } + } + } - Spacer(modifier = Modifier.height(10.dp)) + Spacer(modifier = Modifier.height(10.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - ) { - Column( - modifier = Modifier.verticalScroll(rememberScrollState()), - ) { - Row(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.animateContentSize()) { - FlowRow( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - ) { - postViewModel.amountSet.forEach { amountInSats -> - Button( - modifier = Modifier.padding(horizontal = 3.dp), - shape = ButtonBorder, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - ), - onClick = { postViewModel.removeAmount(amountInSats) }, - ) { - Text( - "⚡ ${ - showAmount( - amountInSats.toBigDecimal().setScale(1), - ) - } ✖", - color = Color.White, - textAlign = TextAlign.Center, - ) - } - } - } - } - } + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + OutlinedTextField( + label = { Text(text = stringRes(R.string.new_amount_in_sats)) }, + value = postViewModel.nextAmount, + onValueChange = { postViewModel.nextAmount = it }, + keyboardOptions = + KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.None, + keyboardType = KeyboardType.Number, + ), + placeholder = { + Text( + text = "100, 1000, 5000", + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + singleLine = true, + modifier = Modifier.padding(end = 10.dp).weight(1f), + ) - Spacer(modifier = Modifier.height(10.dp)) + Button( + onClick = { postViewModel.addAmount() }, + shape = ButtonBorder, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), + ) { + Text(text = stringRes(R.string.add), color = Color.White) + } + } - Row( - modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - label = { Text(text = stringRes(R.string.new_amount_in_sats)) }, - value = postViewModel.nextAmount, - onValueChange = { postViewModel.nextAmount = it }, - keyboardOptions = - KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.None, - keyboardType = KeyboardType.Number, - ), - placeholder = { - Text( - text = "100, 1000, 5000", - color = MaterialTheme.colorScheme.placeholderText, - ) - }, - singleLine = true, - modifier = Modifier.padding(end = 10.dp).weight(1f), + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + TextSpinner( + label = stringRes(id = R.string.zap_type_explainer), + placeholder = + zapTypes.filter { it.first == accountViewModel.defaultZapType() }.first().second, + options = zapOptions, + onSelect = { postViewModel.selectedZapType = zapTypes[it].first }, + modifier = Modifier.weight(1f).padding(end = 5.dp), + ) + } + + HorizontalDivider( + modifier = Modifier.padding(vertical = 10.dp), + thickness = DividerThickness, + ) + + var qrScanning by remember { mutableStateOf(false) } + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + stringRes(id = R.string.wallet_connect_service), + Modifier.weight(1f), + ) + + IconButton( + onClick = { + onClose() + runCatching { uri.openUri("https://nwc.getalby.com/apps/new?c=Amethyst") } + }, + ) { + Icon( + painter = painterResource(R.drawable.alby), + contentDescription = stringRes(id = R.string.accessibility_navigate_to_alby), + modifier = Modifier.size(24.dp), + tint = Color.Unspecified, + ) + } + + IconButton( + onClick = { + clipboardManager.getText()?.let { postViewModel.copyFromClipboard(it.text) } + }, + ) { + Icon( + Icons.Outlined.ContentPaste, + contentDescription = stringRes(id = R.string.paste_from_clipboard), + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary, + ) + } + + IconButton(onClick = { qrScanning = true }) { + Icon( + painter = painterResource(R.drawable.ic_qrcode), + contentDescription = stringRes(id = R.string.accessibility_scan_qr_code), + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary, + ) + } + } + + Row( + modifier = Modifier.fillMaxWidth().padding(bottom = 5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + stringRes(id = R.string.wallet_connect_service_explainer), + Modifier.weight(1f), + color = MaterialTheme.colorScheme.placeholderText, + fontSize = Font14SP, + ) + } + + if (qrScanning) { + SimpleQrCodeScanner { + qrScanning = false + if (!it.isNullOrEmpty()) { + try { + postViewModel.updateNIP47(it) + } catch (e: IllegalArgumentException) { + if (e.message != null) { + accountViewModel.toast( + stringRes(context, R.string.error_parsing_nip47_title), + stringRes(context, R.string.error_parsing_nip47, it, e.message!!), ) - - Button( - onClick = { postViewModel.addAmount() }, - shape = ButtonBorder, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - ), - ) { - Text(text = stringRes(R.string.add), color = Color.White) - } - } - - Row( - modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - TextSpinner( - label = stringRes(id = R.string.zap_type_explainer), - placeholder = - zapTypes.filter { it.first == accountViewModel.defaultZapType() }.first().second, - options = zapOptions, - onSelect = { postViewModel.selectedZapType = zapTypes[it].first }, - modifier = Modifier.weight(1f).padding(end = 5.dp), - ) - } - - HorizontalDivider( - modifier = Modifier.padding(vertical = 10.dp), - thickness = DividerThickness, - ) - - var qrScanning by remember { mutableStateOf(false) } - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - stringRes(id = R.string.wallet_connect_service), - Modifier.weight(1f), - ) - - IconButton( - onClick = { - onClose() - runCatching { uri.openUri("https://nwc.getalby.com/apps/new?c=Amethyst") } - }, - ) { - Icon( - painter = painterResource(R.drawable.alby), - contentDescription = stringRes(id = R.string.accessibility_navigate_to_alby), - modifier = Modifier.size(24.dp), - tint = Color.Unspecified, - ) - } - - IconButton( - onClick = { - clipboardManager.getText()?.let { postViewModel.copyFromClipboard(it.text) } - }, - ) { - Icon( - Icons.Outlined.ContentPaste, - contentDescription = stringRes(id = R.string.paste_from_clipboard), - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary, - ) - } - - IconButton(onClick = { qrScanning = true }) { - Icon( - painter = painterResource(R.drawable.ic_qrcode), - contentDescription = stringRes(id = R.string.accessibility_scan_qr_code), - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary, - ) - } - } - - Row( - modifier = Modifier.fillMaxWidth().padding(bottom = 5.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - stringRes(id = R.string.wallet_connect_service_explainer), - Modifier.weight(1f), - color = MaterialTheme.colorScheme.placeholderText, - fontSize = Font14SP, - ) - } - - if (qrScanning) { - SimpleQrCodeScanner { - qrScanning = false - if (!it.isNullOrEmpty()) { - try { - postViewModel.updateNIP47(it) - } catch (e: IllegalArgumentException) { - if (e.message != null) { - accountViewModel.toast( - stringRes(context, R.string.error_parsing_nip47_title), - stringRes(context, R.string.error_parsing_nip47, it, e.message!!), - ) - } else { - accountViewModel.toast( - stringRes(context, R.string.error_parsing_nip47_title), - stringRes(context, R.string.error_parsing_nip47_no_error, it), - ) - } - } - } - } - } - - Row( - modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - label = { Text(text = stringRes(R.string.wallet_connect_service_pubkey)) }, - value = postViewModel.walletConnectPubkey, - onValueChange = { postViewModel.walletConnectPubkey = it }, - keyboardOptions = - KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.None, - ), - placeholder = { - Text( - text = "npub, hex", - color = MaterialTheme.colorScheme.placeholderText, - ) - }, - singleLine = true, - modifier = Modifier.weight(1f), - ) - } - - Row( - modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - label = { Text(text = stringRes(R.string.wallet_connect_service_relay)) }, - modifier = Modifier.weight(1f), - value = postViewModel.walletConnectRelay, - onValueChange = { postViewModel.walletConnectRelay = it }, - placeholder = { - Text( - text = "wss://relay.server.com", - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 1, - ) - }, - singleLine = true, - ) - } - - var showPassword by remember { mutableStateOf(false) } - - val context = LocalContext.current - - val keyguardLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> - if (result.resultCode == Activity.RESULT_OK) { - showPassword = true - } - } - - val authTitle = stringRes(id = R.string.wallet_connect_service_show_secret) - - Row( - modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - label = { Text(text = stringRes(R.string.wallet_connect_service_secret)) }, - modifier = Modifier.weight(1f), - value = postViewModel.walletConnectSecret, - onValueChange = { postViewModel.walletConnectSecret = it }, - keyboardOptions = - KeyboardOptions( - autoCorrect = false, - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Go, - ), - placeholder = { - Text( - text = stringRes(R.string.wallet_connect_service_secret_placeholder), - color = MaterialTheme.colorScheme.placeholderText, - ) - }, - trailingIcon = { - IconButton( - onClick = { - if (!showPassword) { - authenticate( - title = authTitle, - context = context, - keyguardLauncher = keyguardLauncher, - onApproved = { showPassword = true }, - onError = { title, message -> accountViewModel.toast(title, message) }, - ) - } else { - showPassword = false - } - }, - ) { - Icon( - imageVector = - if (showPassword) { - Icons.Outlined.VisibilityOff - } else { - Icons.Outlined.Visibility - }, - contentDescription = - if (showPassword) { - stringRes(R.string.show_password) - } else { - stringRes( - R.string.hide_password, - ) - }, - ) - } - }, - visualTransformation = - if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), + } else { + accountViewModel.toast( + stringRes(context, R.string.error_parsing_nip47_title), + stringRes(context, R.string.error_parsing_nip47_no_error, it), ) } } } } } + + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + OutlinedTextField( + label = { Text(text = stringRes(R.string.wallet_connect_service_pubkey)) }, + value = postViewModel.walletConnectPubkey, + onValueChange = { postViewModel.walletConnectPubkey = it }, + keyboardOptions = + KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.None, + ), + placeholder = { + Text( + text = "npub, hex", + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + singleLine = true, + modifier = Modifier.weight(1f), + ) + } + + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + OutlinedTextField( + label = { Text(text = stringRes(R.string.wallet_connect_service_relay)) }, + modifier = Modifier.weight(1f), + value = postViewModel.walletConnectRelay, + onValueChange = { postViewModel.walletConnectRelay = it }, + placeholder = { + Text( + text = "wss://relay.server.com", + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 1, + ) + }, + singleLine = true, + ) + } + + var showPassword by remember { mutableStateOf(false) } + + val context = LocalContext.current + + val keyguardLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + if (result.resultCode == Activity.RESULT_OK) { + showPassword = true + } + } + + val authTitle = stringRes(id = R.string.wallet_connect_service_show_secret) + + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + OutlinedTextField( + label = { Text(text = stringRes(R.string.wallet_connect_service_secret)) }, + modifier = Modifier.weight(1f), + value = postViewModel.walletConnectSecret, + onValueChange = { postViewModel.walletConnectSecret = it }, + keyboardOptions = + KeyboardOptions( + autoCorrect = false, + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Go, + ), + placeholder = { + Text( + text = stringRes(R.string.wallet_connect_service_secret_placeholder), + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + trailingIcon = { + IconButton( + onClick = { + if (!showPassword) { + authenticate( + title = authTitle, + context = context, + keyguardLauncher = keyguardLauncher, + onApproved = { showPassword = true }, + onError = { title, message -> accountViewModel.toast(title, message) }, + ) + } else { + showPassword = false + } + }, + ) { + Icon( + imageVector = + if (showPassword) { + Icons.Outlined.VisibilityOff + } else { + Icons.Outlined.Visibility + }, + contentDescription = + if (showPassword) { + stringRes(R.string.show_password) + } else { + stringRes( + R.string.hide_password, + ) + }, + ) + } + }, + visualTransformation = + if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), + ) + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 0c9538c81..78f375bee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -27,6 +27,7 @@ import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences +import com.vitorpamplona.amethyst.LocalPreferences.currentAccount import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.DefaultChannels import com.vitorpamplona.amethyst.model.DefaultDMRelayList @@ -321,11 +322,30 @@ class AccountStateViewModel : ViewModel() { } } + fun currentAccount() = + when (val state = _accountContent.value) { + is AccountState.LoggedIn -> + state.accountSettings.keyPair.pubKey + .toNpub() + + is AccountState.LoggedInViewOnly -> + state.accountSettings.keyPair.pubKey + .toNpub() + + else -> null + } + fun logOff(accountInfo: AccountInfo) { viewModelScope.launch(Dispatchers.IO) { - prepareLogoutOrSwitch() - LocalPreferences.updatePrefsForLogout(accountInfo) - tryLoginExistingAccount() + if (accountInfo.npub == currentAccount()) { + // log off and relogin with the 0 account + prepareLogoutOrSwitch() + LocalPreferences.updatePrefsForLogout(accountInfo) + tryLoginExistingAccount() + } else { + // delete without login off + LocalPreferences.updatePrefsForLogout(accountInfo) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt index 4b3d98539..94af2c47b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt @@ -63,7 +63,6 @@ import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.Route -import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold import com.vitorpamplona.amethyst.ui.stringRes @@ -78,10 +77,7 @@ import kotlinx.coroutines.launch fun HomeScreen( accountViewModel: AccountViewModel, nav: INav, - nip47: String? = null, ) { - ResolveNIP47(nip47, accountViewModel) - HomeScreen( newThreadsFeedState = accountViewModel.feedStates.homeNewThreads, repliesFeedState = accountViewModel.feedStates.homeReplies, @@ -139,18 +135,6 @@ private fun AssembleHomeTabs( inner(pagerState, tabs) } -@Composable -fun ResolveNIP47( - nip47: String?, - accountViewModel: AccountViewModel, -) { - var wantsToAddNip47 by remember(nip47) { mutableStateOf(nip47) } - - if (wantsToAddNip47 != null) { - UpdateZapAmountDialog({ wantsToAddNip47 = null }, wantsToAddNip47, accountViewModel) - } -} - @Composable private fun WatchLifeCycleChanges(accountViewModel: AccountViewModel) { val lifeCycleOwner = LocalLifecycleOwner.current @@ -169,7 +153,6 @@ private fun WatchLifeCycleChanges(accountViewModel: AccountViewModel) { } @Composable -@OptIn(ExperimentalFoundationApi::class) private fun HomePages( pagerState: PagerState, tabs: ImmutableList, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt new file mode 100644 index 000000000..605881139 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.actions.SaveButton +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.rememberHeightDecreaser +import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon +import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountContent +import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun NIP47SetupScreen( + accountViewModel: AccountViewModel, + nav: INav, + nip47: String?, +) { + val postViewModel: UpdateZapAmountViewModel = + viewModel( + key = "UpdateZapAmountViewModel", + factory = UpdateZapAmountViewModel.Factory(accountViewModel.account.settings), + ) + + Scaffold( + topBar = { + TopAppBar( + scrollBehavior = rememberHeightDecreaser(), + title = { Text(stringRes(id = R.string.wallet_connect)) }, + navigationIcon = { + IconButton( + onClick = { + postViewModel.cancel() + nav.popBack() + }, + modifier = Modifier, + ) { + ArrowBackIcon() + } + }, + actions = { + SaveButton( + onPost = { + postViewModel.sendPost() + nav.popBack() + }, + isActive = postViewModel.hasChanged(), + ) + }, + ) + }, + ) { + Column(Modifier.padding(it)) { + UpdateZapAmountContent(postViewModel, onClose = { + postViewModel.cancel() + nav.popBack() + }, nip47, accountViewModel) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/PagerStateHack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/PagerStateHack.kt new file mode 100644 index 000000000..2c6b87cd9 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/PagerStateHack.kt @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.video + +import androidx.annotation.FloatRange +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.gestures.ScrollScope +import androidx.compose.foundation.pager.PagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable +import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState +import kotlin.math.abs + +/** + * This file only exists to fix an interference between the Disappearing Top + * and Bottom Scaffold bars and the offsetFraction of the pager. The current + * implementation ends the scroll at a state where some fraction is still present + * which places videos away from their natural position in the page. + * + * The fix simply animates it back to the fraction = 0. + */ + +@Composable +fun myRememberPagerState( + initialPage: Int = 0, + @FloatRange(from = -0.5, to = 0.5) initialPageOffsetFraction: Float = 0f, + pageCount: () -> Int, +): PagerState = + rememberSaveable(saver = DefaultPagerState.Saver) { + DefaultPagerState( + initialPage, + initialPageOffsetFraction, + pageCount, + ) + }.apply { + pageCountState.value = pageCount + } + +@Composable +fun myRememberForeverPagerState( + key: String, + initialFirstVisibleItemIndex: Int = 0, + initialFirstVisibleItemScrollOffset: Float = 0.0f, + pageCount: () -> Int, +): PagerState = + rememberForeverPagerState(key, initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset, pageCount) { initialPage, initialPageOffsetFraction, pageCount -> + myRememberPagerState(initialPage, initialPageOffsetFraction, pageCount) + } + +private class DefaultPagerState( + currentPage: Int, + currentPageOffsetFraction: Float, + updatedPageCount: () -> Int, +) : PagerState(currentPage, currentPageOffsetFraction) { + var pageCountState = mutableStateOf(updatedPageCount) + override val pageCount: Int get() = pageCountState.value.invoke() + + override suspend fun scroll( + scrollPriority: MutatePriority, + block: suspend ScrollScope.() -> Unit, + ) { + super.scroll(scrollPriority, block) + if (abs(currentPageOffsetFraction) > 0) { + animateScrollToPage(currentPage, 0f) + } + } + + companion object { + /** + * To keep current page and current page offset saved + */ + val Saver: Saver = + listSaver( + save = { + listOf( + it.currentPage, + (it.currentPageOffsetFraction).coerceIn(-0.5f, 0.5f), + it.pageCount, + ) + }, + restore = { + DefaultPagerState( + currentPage = it[0] as Int, + currentPageOffsetFraction = it[1] as Float, + updatedPageCount = { it[2] as Int }, + ) + }, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt index ea2d0f342..c1b370ae8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video import androidx.compose.animation.core.tween -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -33,7 +32,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.VerticalPager -import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.Icon @@ -47,11 +45,11 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.FeatureSetType @@ -69,7 +67,6 @@ import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop -import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.Route @@ -236,7 +233,6 @@ private fun LoadedState( } } -@OptIn(ExperimentalFoundationApi::class) @Composable fun SlidingCarousel( loaded: FeedState.Loaded, @@ -249,9 +245,9 @@ fun SlidingCarousel( val pagerState = if (pagerStateKey != null) { - rememberForeverPagerState(pagerStateKey, items.list.size) { items.list.size } + myRememberForeverPagerState(pagerStateKey, items.list.size) { items.list.size } } else { - rememberPagerState(items.list.size) { items.list.size } + myRememberPagerState(items.list.size) { items.list.size } } WatchScrollToTop(videoFeedContentState, pagerState) diff --git a/amethyst/src/main/res/values-cs/strings.xml b/amethyst/src/main/res/values-cs/strings.xml index 2ed6e1347..ddb83874e 100644 --- a/amethyst/src/main/res/values-cs/strings.xml +++ b/amethyst/src/main/res/values-cs/strings.xml @@ -518,6 +518,7 @@ Světlý Tmavý Předvolby aplikace + Připojení peněženky Jazyk Motiv Automaticky načítat obrázky/gif diff --git a/amethyst/src/main/res/values-de/strings.xml b/amethyst/src/main/res/values-de/strings.xml index 51455ba1a..e1d1084e5 100644 --- a/amethyst/src/main/res/values-de/strings.xml +++ b/amethyst/src/main/res/values-de/strings.xml @@ -523,6 +523,7 @@ anz der Bedingungen ist erforderlich Hell Dunkel Anwendungseinstellungen + Wallet Verbindung Sprache Design Bilder/GIFs automatisch laden diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index ae06d2755..3563777f3 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -518,6 +518,7 @@ Claro Escuro Preferências do aplicativo + Conectar Carteira Linguagem Tema Carregar imagens/gifs automaticamente diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index e57cd2a47..9688faeb9 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -517,6 +517,7 @@ Ljus Mörk Applikationsinställningar + Plånboksanslut Språk Tema Ladda automatiskt bilder/gif diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index ef7db58aa..ff93cb09a 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -121,6 +121,8 @@ 保存到相册 图片已保存到相册 保存图片失败 + 视频已保存到媒体库 + 保存视频失败 上传图片 上传中… 用户尚未设置闪电地址以接收聪 @@ -358,8 +360,11 @@ 删除媒体服务器 你的中继器 (NIP-95) 文件由你的中继器托管。新的 NIP:检查它们是否支持 + 隐私选项 Tor/Orbot 设置 通过你的 Orbot 设置连接 + 调整 + Tor 设置 断开与你的 Orbot/Tor 连接? 你的数据将立即在普通网络上传输 @@ -368,6 +373,7 @@ 所有关注 全球 静音列表 + 默认端口为 9050 ## 通过 Orbot 连线 Tor \n\n1. 安装 [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android) \n2. 开启 Orbot @@ -377,6 +383,45 @@ \n6. 按“启用”按钮以使用 Orbot 作为代理 Orbot Socks 端口 + 启动 Tor + 使用内置的版本或者 Orbot + Tor 和隐私预设 + 快速修改下方的设置 + Onion 链接或中继地址 + 为 .onion 链接使用 Tor + 私信中继 + 强制在收发私信时使用 Tor + 不受信任的中继 + 强制对收件箱和发件箱中继使用 Tor + 可信中继 + 强制为所有中继使用 Tor + 用户资料图片 + 强制为下载用户资料图片时使用 Tor + 链接预览 + 强制为链接预览使用 Tor + 图片 + 强制为加载图片时使用 Tor + 视频 + 强制为加载视频时使用 Tor + 资金操作 + 强制为 Zap、闪电网络和 Cashu 转账时使用 Tor + Nostr 地址验证 + 强制为 NIP-05 地址验证使用 Tor + 媒体上传 + 强制为上传媒体内容时使用 Tor + 内置 + Orbot + 关闭 + 基础 + 默认 + 除多媒体外的所有内容 + 完全隐私 + 自定义 + 当服务器要求时使用 Tor + 从随机中继中隐藏 IP + 除了图片和视频外,总是隐藏 IP + 为所有连接隐藏 IP + 让你自己来设置 无效端口 使用 Orbot 断开 Tor/Orbot 连接 @@ -462,6 +507,7 @@ 此社区没有描述或规则。联系群主来添加 敏感内容 在显示此内容之前添加敏感内容警告 + 偏好设置 设置 始终 仅限 WiFi diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index e205c7c20..93f1b73d2 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -610,6 +610,7 @@ Light Dark Application Preferences + Wallet Connect Language Theme Image Preview