Merge branch 'main' into refactor-media-compressor

This commit is contained in:
David Kaspar
2024-10-09 00:16:35 +01:00
committed by GitHub
19 changed files with 687 additions and 408 deletions
@@ -57,6 +57,7 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -74,7 +75,7 @@ data class AccountInfo(
val npub: String, val npub: String,
val hasPrivKey: Boolean, val hasPrivKey: Boolean,
val loggedInWithExternalSigner: Boolean, val loggedInWithExternalSigner: Boolean,
val isTransient: Boolean = false, val isTransient: Boolean,
) )
private object PrefKeys { private object PrefKeys {
@@ -126,7 +127,7 @@ object LocalPreferences {
private const val COMMA = "," private const val COMMA = ","
private var currentAccount: String? = null private var currentAccount: String? = null
private var savedAccounts: List<AccountInfo>? = null private var savedAccounts: MutableStateFlow<List<AccountInfo>?> = MutableStateFlow(null)
private var cachedAccounts: MutableMap<String, AccountSettings?> = mutableMapOf() private var cachedAccounts: MutableMap<String, AccountSettings?> = mutableMapOf()
suspend fun currentAccount(): String? { suspend fun currentAccount(): String? {
@@ -156,7 +157,7 @@ object LocalPreferences {
} }
private suspend fun savedAccounts(): List<AccountInfo> { private suspend fun savedAccounts(): List<AccountInfo> {
if (savedAccounts == null) { if (savedAccounts.value == null) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
with(encryptedPreferences()) { with(encryptedPreferences()) {
val newSystemOfAccounts = val newSystemOfAccounts =
@@ -165,7 +166,7 @@ object LocalPreferences {
} }
if (!newSystemOfAccounts.isNullOrEmpty()) { if (!newSystemOfAccounts.isNullOrEmpty()) {
savedAccounts = newSystemOfAccounts savedAccounts.emit(newSystemOfAccounts)
} else { } else {
val oldAccounts = getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(COMMA) ?: listOf() val oldAccounts = getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(COMMA) ?: listOf()
@@ -174,27 +175,28 @@ object LocalPreferences {
AccountInfo( AccountInfo(
npub, npub,
encryptedPreferences(npub).getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false), encryptedPreferences(npub).getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false),
(encryptedPreferences(npub).getString(PrefKeys.NOSTR_PRIVKEY, "") ?: "") (encryptedPreferences(npub).getString(PrefKeys.NOSTR_PRIVKEY, "") ?: "").isNotBlank(),
.isNotBlank(), false,
) )
} }
savedAccounts = migrated savedAccounts.emit(migrated)
edit().apply { putString(PrefKeys.ALL_ACCOUNT_INFO, Event.mapper.writeValueAsString(savedAccounts)) }.apply() 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<AccountInfo>) = private suspend fun updateSavedAccounts(accounts: List<AccountInfo>) =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (savedAccounts != accounts) { if (savedAccounts != accounts) {
savedAccounts = accounts savedAccounts.emit(accounts)
encryptedPreferences() encryptedPreferences()
.edit() .edit()
@@ -269,7 +271,7 @@ object LocalPreferences {
*/ */
@SuppressLint("ApplySharedPref") @SuppressLint("ApplySharedPref")
suspend fun updatePrefsForLogout(accountInfo: AccountInfo) { 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) { withContext(Dispatchers.IO) {
encryptedPreferences(accountInfo.npub).edit().clear().commit() encryptedPreferences(accountInfo.npub).edit().clear().commit()
removeAccount(accountInfo) removeAccount(accountInfo)
@@ -324,7 +324,7 @@ fun uriToRoute(uri: String?): String? =
uri?.let { uri?.let {
Nip47WalletConnect.parse(it) Nip47WalletConnect.parse(it)
val encodedUri = URLEncoder.encode(it, StandardCharsets.UTF_8.toString()) val encodedUri = URLEncoder.encode(it, StandardCharsets.UTF_8.toString())
Route.Home.base + "?nip47=" + encodedUri Route.NIP47Setup.base + "?nip47=" + encodedUri
} }
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
@@ -161,9 +161,9 @@ fun ZoomableContentView(
nostrUriCallback = content.uri, nostrUriCallback = content.uri,
onDialog = { onDialog = {
dialogOpen = true dialogOpen = true
if (!isFoldableOrLarge && !isOrientationLocked) { // if (!isFoldableOrLarge && !isOrientationLocked) {
DeviceUtils.changeDeviceOrientation(isLandscapeMode, activity) // DeviceUtils.changeDeviceOrientation(isLandscapeMode, activity)
} // }
}, },
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) )
@@ -201,7 +201,7 @@ fun ZoomableContentView(
images, images,
onDismiss = { onDismiss = {
dialogOpen = false dialogOpen = false
if (!isFoldableOrLarge && !isOrientationLocked) DeviceUtils.changeDeviceOrientation(isLandscapeMode, activity) // if (!isFoldableOrLarge && !isOrientationLocked) DeviceUtils.changeDeviceOrientation(isLandscapeMode, activity)
}, },
accountViewModel, accountViewModel,
) )
@@ -20,7 +20,6 @@
*/ */
package com.vitorpamplona.amethyst.ui.feeds package com.vitorpamplona.amethyst.ui.feeds
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
@@ -115,20 +114,35 @@ fun rememberForeverLazyListState(
return scrollState return scrollState
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun rememberForeverPagerState( fun rememberForeverPagerState(
key: String, key: String,
initialFirstVisibleItemIndex: Int = 0, initialFirstVisibleItemIndex: Int = 0,
initialFirstVisibleItemScrollOffset: Float = 0.0f, initialFirstVisibleItemScrollOffset: Float = 0.0f,
pageCount: () -> Int, 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 { ): PagerState {
val savedValue = savedScrollStates[key] val savedValue = savedScrollStates[key]
val savedIndex = savedValue?.index ?: initialFirstVisibleItemIndex val savedIndex = savedValue?.index ?: initialFirstVisibleItemIndex
val savedOffset = savedValue?.scrollOffsetFraction ?: initialFirstVisibleItemScrollOffset val savedOffset = savedValue?.scrollOffsetFraction ?: initialFirstVisibleItemScrollOffset
val scrollState = val scrollState =
rememberPagerState( rememberPagerStateFunction(
savedIndex, savedIndex,
savedOffset, savedOffset,
pageCount, pageCount,
@@ -47,7 +47,6 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment 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.font.FontWeight
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 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
@@ -73,6 +73,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.forEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -81,14 +82,6 @@ fun AccountSwitchBottomSheet(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
accountStateViewModel: AccountStateViewModel, accountStateViewModel: AccountStateViewModel,
) { ) {
@Suppress("ProduceStateDoesNotAssignValue")
val accounts by
produceState(initialValue = LocalPreferences.cachedAccounts()) {
if (value == null) {
value = LocalPreferences.allSavedAccounts()
}
}
var popupExpanded by remember { mutableStateOf(false) } var popupExpanded by remember { mutableStateOf(false) }
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
@@ -103,7 +96,7 @@ fun AccountSwitchBottomSheet(
) { ) {
Text(stringRes(R.string.account_switch_select_account), fontWeight = FontWeight.Bold) Text(stringRes(R.string.account_switch_select_account), fontWeight = FontWeight.Bold)
} }
accounts?.forEach { acc -> DisplayAccount(acc, accountViewModel, accountStateViewModel) } DisplayAllAccounts(accountViewModel, accountStateViewModel)
Row( Row(
modifier = modifier =
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 @Composable
fun DisplayAccount( fun DisplayAccount(
acc: AccountInfo, acc: AccountInfo,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
accountStateViewModel: AccountStateViewModel, accountStateViewModel: AccountStateViewModel,
) { ) {
var baseUser by remember { var baseUser by remember(acc) {
mutableStateOf<User?>( mutableStateOf<User?>(
decodePublicKeyAsHexOrNull(acc.npub)?.let { decodePublicKeyAsHexOrNull(acc.npub)?.let {
LocalCache.getUserIfExists(it) LocalCache.getUserIfExists(it)
@@ -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.notifications.NotificationScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ProfileScreen 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.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.SecurityFiltersScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.ThreadScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.ThreadScreen
@@ -100,24 +101,7 @@ fun AppNavigation(
enterTransition = { fadeIn(animationSpec = tween(200)) }, enterTransition = { fadeIn(animationSpec = tween(200)) },
exitTransition = { fadeOut(animationSpec = tween(200)) }, exitTransition = { fadeOut(animationSpec = tween(200)) },
) { ) {
composable( composable(Route.Home.route) { HomeScreen(accountViewModel, nav) }
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.Message.route) { ChatroomListScreen(accountViewModel, nav) } composable(Route.Message.route) { ChatroomListScreen(accountViewModel, nav) }
composable(Route.Video.route) { VideoScreen(accountViewModel, nav) } composable(Route.Video.route) { VideoScreen(accountViewModel, nav) }
composable(Route.Discover.route) { DiscoverScreen(accountViewModel, nav) } composable(Route.Discover.route) { DiscoverScreen(accountViewModel, nav) }
@@ -282,6 +266,19 @@ fun AppNavigation(
nav, 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)
}
} }
} }
@@ -52,18 +52,10 @@ sealed class Route(
) { ) {
object Home : object Home :
Route( Route(
route = "Home?nip47={nip47}", route = "Home",
icon = R.drawable.ic_home, icon = R.drawable.ic_home,
notifSize = Modifier.size(Size25dp), notifSize = Modifier.size(Size25dp),
iconSize = Modifier.size(Size24dp), iconSize = Modifier.size(Size24dp),
arguments =
listOf(
navArgument("nip47") {
type = NavType.StringType
nullable = true
defaultValue = null
},
).toImmutableList(),
contentDescriptor = R.string.route_home, contentDescriptor = R.string.route_home,
) )
@@ -92,9 +84,6 @@ sealed class Route(
Route( Route(
route = "Discover", route = "Discover",
icon = R.drawable.ic_sensors, icon = R.drawable.ic_sensors,
// hasNewItems = { accountViewModel, newNotes ->
// DiscoverLatestItem.hasNewItems(accountViewModel, newNotes)
// },
contentDescriptor = R.string.route_discover, contentDescriptor = R.string.route_discover,
) )
@@ -217,6 +206,20 @@ sealed class Route(
route = "Settings", route = "Settings",
icon = R.drawable.ic_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? { fun getRouteWithArguments(navController: NavHostController): String? {
@@ -31,7 +31,6 @@ import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt
import androidx.compose.animation.animateContentSize
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.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
@@ -220,16 +219,66 @@ fun UpdateZapAmountDialog(
onClose: () -> Unit, onClose: () -> Unit,
nip47uri: String? = null, nip47uri: String? = null,
accountViewModel: AccountViewModel, 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 context = LocalContext.current
val clipboardManager = LocalClipboardManager.current val clipboardManager = LocalClipboardManager.current
val postViewModel: UpdateZapAmountViewModel =
viewModel(
key = "UpdateZapAmountViewModel",
factory = UpdateZapAmountViewModel.Factory(accountViewModel.account.settings),
)
val uri = LocalUriHandler.current val uri = LocalUriHandler.current
val zapTypes = val zapTypes =
@@ -282,337 +331,295 @@ fun UpdateZapAmountDialog(
} }
} }
Dialog( Column(
onDismissRequest = { onClose() }, modifier =
properties = Modifier
DialogProperties( .padding(10.dp)
usePlatformDefaultWidth = false, .fillMaxWidth()
dismissOnClickOutside = false, .imePadding()
decorFitsSystemWindows = false, .verticalScroll(rememberScrollState()),
),
) { ) {
Surface( FlowRow(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
) { ) {
Column(modifier = Modifier.padding(10.dp).imePadding()) { postViewModel.amountSet.forEach { amountInSats ->
Row( Button(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.padding(horizontal = 3.dp),
horizontalArrangement = Arrangement.SpaceBetween, shape = ButtonBorder,
verticalAlignment = Alignment.CenterVertically, colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
),
onClick = { postViewModel.removeAmount(amountInSats) },
) { ) {
CloseButton( Text(
onPress = { "${
postViewModel.cancel() showAmount(
onClose() amountInSats.toBigDecimal().setScale(1),
}, )
) } ",
color = Color.White,
SaveButton( textAlign = TextAlign.Center,
onPost = {
postViewModel.sendPost()
onClose()
},
isActive = postViewModel.hasChanged(),
) )
} }
}
}
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp),
) { verticalAlignment = Alignment.CenterVertically,
Column( ) {
modifier = Modifier.verticalScroll(rememberScrollState()), OutlinedTextField(
) { label = { Text(text = stringRes(R.string.new_amount_in_sats)) },
Row(modifier = Modifier.fillMaxWidth()) { value = postViewModel.nextAmount,
Column(modifier = Modifier.animateContentSize()) { onValueChange = { postViewModel.nextAmount = it },
FlowRow( keyboardOptions =
modifier = Modifier.fillMaxWidth(), KeyboardOptions.Default.copy(
horizontalArrangement = Arrangement.Center, capitalization = KeyboardCapitalization.None,
) { keyboardType = KeyboardType.Number,
postViewModel.amountSet.forEach { amountInSats -> ),
Button( placeholder = {
modifier = Modifier.padding(horizontal = 3.dp), Text(
shape = ButtonBorder, text = "100, 1000, 5000",
colors = color = MaterialTheme.colorScheme.placeholderText,
ButtonDefaults.buttonColors( )
containerColor = MaterialTheme.colorScheme.primary, },
), singleLine = true,
onClick = { postViewModel.removeAmount(amountInSats) }, modifier = Modifier.padding(end = 10.dp).weight(1f),
) { )
Text(
"${
showAmount(
amountInSats.toBigDecimal().setScale(1),
)
} ",
color = Color.White,
textAlign = TextAlign.Center,
)
}
}
}
}
}
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( Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
OutlinedTextField( TextSpinner(
label = { Text(text = stringRes(R.string.new_amount_in_sats)) }, label = stringRes(id = R.string.zap_type_explainer),
value = postViewModel.nextAmount, placeholder =
onValueChange = { postViewModel.nextAmount = it }, zapTypes.filter { it.first == accountViewModel.defaultZapType() }.first().second,
keyboardOptions = options = zapOptions,
KeyboardOptions.Default.copy( onSelect = { postViewModel.selectedZapType = zapTypes[it].first },
capitalization = KeyboardCapitalization.None, modifier = Modifier.weight(1f).padding(end = 5.dp),
keyboardType = KeyboardType.Number, )
), }
placeholder = {
Text( HorizontalDivider(
text = "100, 1000, 5000", modifier = Modifier.padding(vertical = 10.dp),
color = MaterialTheme.colorScheme.placeholderText, thickness = DividerThickness,
) )
},
singleLine = true, var qrScanning by remember { mutableStateOf(false) }
modifier = Modifier.padding(end = 10.dp).weight(1f),
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 {
Button( accountViewModel.toast(
onClick = { postViewModel.addAmount() }, stringRes(context, R.string.error_parsing_nip47_title),
shape = ButtonBorder, stringRes(context, R.string.error_parsing_nip47_no_error, it),
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(),
) )
} }
} }
} }
} }
} }
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(),
)
}
} }
} }
@@ -27,6 +27,7 @@ import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.AccountInfo
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.LocalPreferences.currentAccount
import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.DefaultChannels import com.vitorpamplona.amethyst.model.DefaultChannels
import com.vitorpamplona.amethyst.model.DefaultDMRelayList 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) { fun logOff(accountInfo: AccountInfo) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
prepareLogoutOrSwitch() if (accountInfo.npub == currentAccount()) {
LocalPreferences.updatePrefsForLogout(accountInfo) // log off and relogin with the 0 account
tryLoginExistingAccount() prepareLogoutOrSwitch()
LocalPreferences.updatePrefsForLogout(accountInfo)
tryLoginExistingAccount()
} else {
// delete without login off
LocalPreferences.updatePrefsForLogout(accountInfo)
}
} }
} }
} }
@@ -63,7 +63,6 @@ import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.Route 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.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
@@ -78,10 +77,7 @@ import kotlinx.coroutines.launch
fun HomeScreen( fun HomeScreen(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
nip47: String? = null,
) { ) {
ResolveNIP47(nip47, accountViewModel)
HomeScreen( HomeScreen(
newThreadsFeedState = accountViewModel.feedStates.homeNewThreads, newThreadsFeedState = accountViewModel.feedStates.homeNewThreads,
repliesFeedState = accountViewModel.feedStates.homeReplies, repliesFeedState = accountViewModel.feedStates.homeReplies,
@@ -139,18 +135,6 @@ private fun AssembleHomeTabs(
inner(pagerState, tabs) 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 @Composable
private fun WatchLifeCycleChanges(accountViewModel: AccountViewModel) { private fun WatchLifeCycleChanges(accountViewModel: AccountViewModel) {
val lifeCycleOwner = LocalLifecycleOwner.current val lifeCycleOwner = LocalLifecycleOwner.current
@@ -169,7 +153,6 @@ private fun WatchLifeCycleChanges(accountViewModel: AccountViewModel) {
} }
@Composable @Composable
@OptIn(ExperimentalFoundationApi::class)
private fun HomePages( private fun HomePages(
pagerState: PagerState, pagerState: PagerState,
tabs: ImmutableList<TabItem>, tabs: ImmutableList<TabItem>,
@@ -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)
}
}
}
@@ -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<DefaultPagerState, *> =
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 },
)
},
)
}
}
@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.video package com.vitorpamplona.amethyst.ui.screen.loggedIn.video
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.VerticalPager import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -47,11 +45,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
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.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
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.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.RefresheableBox
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop 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.AppBottomBar
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.navigation.Route
@@ -236,7 +233,6 @@ private fun LoadedState(
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun SlidingCarousel( fun SlidingCarousel(
loaded: FeedState.Loaded, loaded: FeedState.Loaded,
@@ -249,9 +245,9 @@ fun SlidingCarousel(
val pagerState = val pagerState =
if (pagerStateKey != null) { if (pagerStateKey != null) {
rememberForeverPagerState(pagerStateKey, items.list.size) { items.list.size } myRememberForeverPagerState(pagerStateKey, items.list.size) { items.list.size }
} else { } else {
rememberPagerState(items.list.size) { items.list.size } myRememberPagerState(items.list.size) { items.list.size }
} }
WatchScrollToTop(videoFeedContentState, pagerState) WatchScrollToTop(videoFeedContentState, pagerState)
@@ -518,6 +518,7 @@
<string name="light">Světlý</string> <string name="light">Světlý</string>
<string name="dark">Tmavý</string> <string name="dark">Tmavý</string>
<string name="application_preferences">Předvolby aplikace</string> <string name="application_preferences">Předvolby aplikace</string>
<string name="wallet_connect">Připojení peněženky</string>
<string name="language">Jazyk</string> <string name="language">Jazyk</string>
<string name="theme">Motiv</string> <string name="theme">Motiv</string>
<string name="automatically_load_images_gifs">Automaticky načítat obrázky/gif</string> <string name="automatically_load_images_gifs">Automaticky načítat obrázky/gif</string>
@@ -523,6 +523,7 @@ anz der Bedingungen ist erforderlich</string>
<string name="light">Hell</string> <string name="light">Hell</string>
<string name="dark">Dunkel</string> <string name="dark">Dunkel</string>
<string name="application_preferences">Anwendungseinstellungen</string> <string name="application_preferences">Anwendungseinstellungen</string>
<string name="wallet_connect">Wallet Verbindung</string>
<string name="language">Sprache</string> <string name="language">Sprache</string>
<string name="theme">Design</string> <string name="theme">Design</string>
<string name="automatically_load_images_gifs">Bilder/GIFs automatisch laden</string> <string name="automatically_load_images_gifs">Bilder/GIFs automatisch laden</string>
@@ -518,6 +518,7 @@
<string name="light">Claro</string> <string name="light">Claro</string>
<string name="dark">Escuro</string> <string name="dark">Escuro</string>
<string name="application_preferences">Preferências do aplicativo</string> <string name="application_preferences">Preferências do aplicativo</string>
<string name="wallet_connect">Conectar Carteira</string>
<string name="language">Linguagem</string> <string name="language">Linguagem</string>
<string name="theme">Tema</string> <string name="theme">Tema</string>
<string name="automatically_load_images_gifs">Carregar imagens/gifs automaticamente</string> <string name="automatically_load_images_gifs">Carregar imagens/gifs automaticamente</string>
@@ -517,6 +517,7 @@
<string name="light">Ljus</string> <string name="light">Ljus</string>
<string name="dark">Mörk</string> <string name="dark">Mörk</string>
<string name="application_preferences">Applikationsinställningar</string> <string name="application_preferences">Applikationsinställningar</string>
<string name="wallet_connect">Plånboksanslut</string>
<string name="language">Språk</string> <string name="language">Språk</string>
<string name="theme">Tema</string> <string name="theme">Tema</string>
<string name="automatically_load_images_gifs">Ladda automatiskt bilder/gif</string> <string name="automatically_load_images_gifs">Ladda automatiskt bilder/gif</string>
@@ -121,6 +121,8 @@
<string name="save_to_gallery">保存到相册</string> <string name="save_to_gallery">保存到相册</string>
<string name="image_saved_to_the_gallery">图片已保存到相册</string> <string name="image_saved_to_the_gallery">图片已保存到相册</string>
<string name="failed_to_save_the_image">保存图片失败</string> <string name="failed_to_save_the_image">保存图片失败</string>
<string name="video_saved_to_the_gallery">视频已保存到媒体库</string>
<string name="failed_to_save_the_video">保存视频失败</string>
<string name="upload_image">上传图片</string> <string name="upload_image">上传图片</string>
<string name="uploading">上传中…</string> <string name="uploading">上传中…</string>
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">用户尚未设置闪电地址以接收聪</string> <string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">用户尚未设置闪电地址以接收聪</string>
@@ -358,8 +360,11 @@
<string name="delete_media_server">删除媒体服务器</string> <string name="delete_media_server">删除媒体服务器</string>
<string name="upload_server_relays_nip95">你的中继器 (NIP-95)</string> <string name="upload_server_relays_nip95">你的中继器 (NIP-95)</string>
<string name="upload_server_relays_nip95_explainer">文件由你的中继器托管。新的 NIP:检查它们是否支持</string> <string name="upload_server_relays_nip95_explainer">文件由你的中继器托管。新的 NIP:检查它们是否支持</string>
<string name="privacy_options">隐私选项</string>
<string name="connect_via_tor_short">Tor/Orbot 设置</string> <string name="connect_via_tor_short">Tor/Orbot 设置</string>
<string name="connect_via_tor">通过你的 Orbot 设置连接</string> <string name="connect_via_tor">通过你的 Orbot 设置连接</string>
<string name="connect_via_tor1">调整</string>
<string name="connect_via_tor2">Tor 设置</string>
<string name="do_you_really_want_to_disable_tor_title">断开与你的 Orbot/Tor 连接?</string> <string name="do_you_really_want_to_disable_tor_title">断开与你的 Orbot/Tor 连接?</string>
<string name="do_you_really_want_to_disable_tor_text">你的数据将立即在普通网络上传输</string> <string name="do_you_really_want_to_disable_tor_text">你的数据将立即在普通网络上传输</string>
<string name="yes"></string> <string name="yes"></string>
@@ -368,6 +373,7 @@
<string name="follow_list_kind3follows">所有关注</string> <string name="follow_list_kind3follows">所有关注</string>
<string name="follow_list_global">全球</string> <string name="follow_list_global">全球</string>
<string name="follow_list_mute_list">静音列表</string> <string name="follow_list_mute_list">静音列表</string>
<string name="connect_through_your_orbot_setup_short">默认端口为 9050</string>
<string name="connect_through_your_orbot_setup_markdown"> ## 通过 Orbot 连线 Tor <string name="connect_through_your_orbot_setup_markdown"> ## 通过 Orbot 连线 Tor
\n\n1. 安装 [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android) \n\n1. 安装 [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
\n2. 开启 Orbot \n2. 开启 Orbot
@@ -377,6 +383,45 @@
\n6. 按“启用”按钮以使用 Orbot 作为代理 \n6. 按“启用”按钮以使用 Orbot 作为代理
</string> </string>
<string name="orbot_socks_port">Orbot Socks 端口</string> <string name="orbot_socks_port">Orbot Socks 端口</string>
<string name="use_internal_tor">启动 Tor</string>
<string name="use_internal_tor_explainer">使用内置的版本或者 Orbot</string>
<string name="tor_preset">Tor 和隐私预设</string>
<string name="tor_preset_explainer">快速修改下方的设置</string>
<string name="tor_use_onion_address">Onion 链接或中继地址</string>
<string name="tor_use_onion_address_explainer">为 .onion 链接使用 Tor</string>
<string name="tor_use_dm_relays">私信中继</string>
<string name="tor_use_dm_relays_explainer">强制在收发私信时使用 Tor</string>
<string name="tor_use_new_relays">不受信任的中继</string>
<string name="tor_use_new_relays_explainer">强制对收件箱和发件箱中继使用 Tor</string>
<string name="tor_use_trusted_relays">可信中继</string>
<string name="tor_use_trusted_relays_explainer">强制为所有中继使用 Tor</string>
<string name="tor_use_profile_pictures">用户资料图片</string>
<string name="tor_use_profile_pictures_explainer">强制为下载用户资料图片时使用 Tor</string>
<string name="tor_use_url_previews">链接预览</string>
<string name="tor_use_url_previews_explainer">强制为链接预览使用 Tor</string>
<string name="tor_use_images">图片</string>
<string name="tor_use_images_explainer">强制为加载图片时使用 Tor</string>
<string name="tor_use_videos">视频</string>
<string name="tor_use_videos_explainer">强制为加载视频时使用 Tor</string>
<string name="tor_use_money_operations">资金操作</string>
<string name="tor_use_money_operations_explainer">强制为 Zap、闪电网络和 Cashu 转账时使用 Tor</string>
<string name="tor_use_nip05_verification">Nostr 地址验证</string>
<string name="tor_use_nip05_verification_explainer">强制为 NIP-05 地址验证使用 Tor</string>
<string name="tor_use_nip96_uploads">媒体上传</string>
<string name="tor_use_nip96_uploads_explainer">强制为上传媒体内容时使用 Tor</string>
<string name="tor_internal">内置</string>
<string name="tor_external">Orbot</string>
<string name="tor_off">关闭</string>
<string name="tor_when_needed">基础</string>
<string name="tor_default">默认</string>
<string name="tor_small_payloads">除多媒体外的所有内容</string>
<string name="tor_full_privacy">完全隐私</string>
<string name="tor_custom">自定义</string>
<string name="tor_when_needed_explainer">当服务器要求时使用 Tor</string>
<string name="tor_default_explainer">从随机中继中隐藏 IP</string>
<string name="tor_small_payloads_explainer">除了图片和视频外,总是隐藏 IP</string>
<string name="tor_full_privacy_explainer">为所有连接隐藏 IP</string>
<string name="tor_custom_explainer">让你自己来设置</string>
<string name="invalid_port_number">无效端口</string> <string name="invalid_port_number">无效端口</string>
<string name="use_orbot">使用 Orbot</string> <string name="use_orbot">使用 Orbot</string>
<string name="disconnect_from_your_orbot_setup">断开 Tor/Orbot 连接</string> <string name="disconnect_from_your_orbot_setup">断开 Tor/Orbot 连接</string>
@@ -462,6 +507,7 @@
<string name="community_no_descriptor">此社区没有描述或规则。联系群主来添加</string> <string name="community_no_descriptor">此社区没有描述或规则。联系群主来添加</string>
<string name="add_sensitive_content_label">敏感内容</string> <string name="add_sensitive_content_label">敏感内容</string>
<string name="add_sensitive_content_description">在显示此内容之前添加敏感内容警告</string> <string name="add_sensitive_content_description">在显示此内容之前添加敏感内容警告</string>
<string name="preferences">偏好设置</string>
<string name="settings">设置</string> <string name="settings">设置</string>
<string name="connectivity_type_always">始终</string> <string name="connectivity_type_always">始终</string>
<string name="connectivity_type_wifi_only">仅限 WiFi</string> <string name="connectivity_type_wifi_only">仅限 WiFi</string>
+1
View File
@@ -610,6 +610,7 @@
<string name="light">Light</string> <string name="light">Light</string>
<string name="dark">Dark</string> <string name="dark">Dark</string>
<string name="application_preferences">Application Preferences</string> <string name="application_preferences">Application Preferences</string>
<string name="wallet_connect">Wallet Connect</string>
<string name="language">Language</string> <string name="language">Language</string>
<string name="theme">Theme</string> <string name="theme">Theme</string>
<string name="automatically_load_images_gifs">Image Preview</string> <string name="automatically_load_images_gifs">Image Preview</string>