Move nsec backup to Drawer and Dialog, organize Drawer

This commit is contained in:
maxmoney21m
2023-03-04 03:02:04 +08:00
parent 0da542fede
commit 3e32a91e34
20 changed files with 271 additions and 119 deletions

View File

@@ -7,12 +7,13 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth 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.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Divider import androidx.compose.material.Divider
import androidx.compose.material.Icon import androidx.compose.material.Icon
@@ -36,8 +37,8 @@ import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.FontWeight.Companion.W500
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.navigation.NavController import androidx.navigation.NavController
@@ -45,14 +46,14 @@ import androidx.navigation.NavHostController
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.RoboHashCache
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.platform.LocalContext
@Composable @Composable
fun DrawerContent(navController: NavHostController, fun DrawerContent(navController: NavHostController,
@@ -88,7 +89,8 @@ fun DrawerContent(navController: NavHostController,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1F), .weight(1F),
accountStateViewModel accountStateViewModel,
account,
) )
BottomContent(account.userProfile(), scaffoldState, navController) BottomContent(account.userProfile(), scaffoldState, navController)
@@ -155,7 +157,9 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol
if (accountUser.bestDisplayName() != null) { if (accountUser.bestDisplayName() != null) {
Text( Text(
accountUser.bestDisplayName() ?: "", accountUser.bestDisplayName() ?: "",
modifier = Modifier.padding(top = 7.dp).clickable(onClick = { modifier = Modifier
.padding(top = 7.dp)
.clickable(onClick = {
accountUser.let { accountUser.let {
navController.navigate("User/${it.pubkeyHex}") navController.navigate("User/${it.pubkeyHex}")
} }
@@ -169,7 +173,9 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol
} }
if (accountUser.bestUsername() != null) { if (accountUser.bestUsername() != null) {
Text(" @${accountUser.bestUsername()}", color = Color.LightGray, Text(" @${accountUser.bestUsername()}", color = Color.LightGray,
modifier = Modifier.padding(top = 15.dp).clickable(onClick = { modifier = Modifier
.padding(top = 15.dp)
.clickable(onClick = {
accountUser.let { accountUser.let {
navController.navigate("User/${it.pubkeyHex}") navController.navigate("User/${it.pubkeyHex}")
} }
@@ -179,7 +185,9 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol
}) })
) )
} }
Row(modifier = Modifier.padding(top = 15.dp).clickable(onClick = { Row(modifier = Modifier
.padding(top = 15.dp)
.clickable(onClick = {
accountUser.let { accountUser.let {
navController.navigate("User/${it.pubkeyHex}") navController.navigate("User/${it.pubkeyHex}")
} }
@@ -206,59 +214,70 @@ fun ListContent(
navController: NavHostController, navController: NavHostController,
scaffoldState: ScaffoldState, scaffoldState: ScaffoldState,
modifier: Modifier, modifier: Modifier,
accountViewModel: AccountStateViewModel accountViewModel: AccountStateViewModel,
account: Account,
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var backupDialogOpen by remember { mutableStateOf(false) }
Column(modifier = modifier) { Column(modifier = modifier.fillMaxHeight()) {
LazyColumn() {
item {
if (accountUser != null) if (accountUser != null)
NavigationRow(navController, NavigationRow(
scaffoldState, title = stringResource(R.string.profile),
"User/${accountUser.pubkeyHex}", icon = Route.Profile.icon,
Route.Profile.icon, tint = MaterialTheme.colors.primary,
stringResource(R.string.profile) navController = navController,
scaffoldState = scaffoldState,
route = "User/${accountUser.pubkeyHex}",
) )
Divider( Divider(thickness = 0.25.dp)
modifier = Modifier.padding(bottom = 15.dp),
thickness = 0.25.dp NavigationRow(
title = stringResource(R.string.security_filters),
icon = Route.Filters.icon,
tint = MaterialTheme.colors.onBackground,
navController = navController,
scaffoldState = scaffoldState,
route = Route.Filters.route,
) )
Column(modifier = modifier.padding(horizontal = 25.dp)) {
Row(modifier = Modifier.clickable(onClick = { Divider(thickness = 0.25.dp)
navController.navigate(Route.Filters.route)
coroutineScope.launch { IconRow(
scaffoldState.drawerState.close() title = "Backup Keys",
} icon = R.drawable.ic_key,
})) { tint = MaterialTheme.colors.onBackground,
Text( onClick = { backupDialogOpen = true }
text = stringResource(R.string.security_filters), )
fontSize = 18.sp,
fontWeight = W500 Spacer(modifier = Modifier.weight(1f))
IconRow(
"Logout",
R.drawable.ic_logout,
MaterialTheme.colors.onBackground,
onClick = { accountViewModel.logOff() }
) )
} }
Row(modifier = Modifier.clickable(onClick = { accountViewModel.logOff() })) {
Text( if (backupDialogOpen) {
text = stringResource(R.string.log_out), AccountBackupDialog(account, onClose = { backupDialogOpen = false })
modifier = Modifier.padding(vertical = 15.dp),
fontSize = 18.sp,
fontWeight = W500
)
}
}
}
}
} }
} }
@Composable @Composable
fun NavigationRow(navController: NavHostController, scaffoldState: ScaffoldState, route: String, icon: Int, title: String) { fun NavigationRow(
title: String,
icon: Int,
tint: Color,
navController: NavHostController,
scaffoldState: ScaffoldState,
route: String,
) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val currentRoute = currentRoute(navController) val currentRoute = currentRoute(navController)
Row(modifier = Modifier IconRow(title, icon, tint, onClick = {
.fillMaxWidth()
.clickable(onClick = {
if (currentRoute != route) { if (currentRoute != route) {
navController.navigate(route) navController.navigate(route)
} }
@@ -266,6 +285,13 @@ fun NavigationRow(navController: NavHostController, scaffoldState: ScaffoldState
scaffoldState.drawerState.close() scaffoldState.drawerState.close()
} }
}) })
}
@Composable
fun IconRow(title: String, icon: Int, tint: Color, onClick: () -> Unit) {
Row(modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -276,7 +302,7 @@ fun NavigationRow(navController: NavHostController, scaffoldState: ScaffoldState
Icon( Icon(
painter = painterResource(icon), null, painter = painterResource(icon), null,
modifier = Modifier.size(22.dp), modifier = Modifier.size(22.dp),
tint = MaterialTheme.colors.primary tint = tint
) )
Text( Text(
modifier = Modifier.padding(start = 16.dp), modifier = Modifier.padding(start = 16.dp),

View File

@@ -53,7 +53,7 @@ sealed class Route(
buildScreen = { acc, accSt, nav -> { _ -> ChatroomListScreen(acc, nav) }} buildScreen = { acc, accSt, nav -> { _ -> ChatroomListScreen(acc, nav) }}
) )
object Filters : Route("Filters", R.drawable.ic_dm, object Filters : Route("Filters", R.drawable.ic_security,
buildScreen = { acc, accSt, nav -> { _ -> FiltersScreen(acc, nav) }} buildScreen = { acc, accSt, nav -> { _ -> FiltersScreen(acc, nav) }}
) )

View File

@@ -0,0 +1,123 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Key
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material.MaterialRichText
import com.halilibo.richtext.ui.resolveDefaults
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import kotlinx.coroutines.launch
import nostr.postr.toNsec
@Composable
fun AccountBackupDialog(account: Account, onClose: () -> Unit) {
Dialog(
onDismissRequest = onClose,
properties = DialogProperties(usePlatformDefaultWidth = false),
) {
Surface(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.background(MaterialTheme.colors.background)
.fillMaxSize(),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = onClose)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
MaterialRichText(
style = RichTextStyle().resolveDefaults(),
) {
Markdown(
content = stringResource(R.string.account_backup_tips_md),
)
}
Spacer(modifier = Modifier.height(15.dp))
NSecCopyButton(account)
}
}
}
}
}
@Composable
private fun NSecCopyButton(
account: Account
) {
val clipboardManager = LocalClipboardManager.current
val context = LocalContext.current
val scope = rememberCoroutineScope()
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = {
account.loggedIn.privKey?.let {
clipboardManager.setText(AnnotatedString(it.toNsec()))
scope.launch {
Toast.makeText(
context,
context.getString(R.string.secret_key_copied_to_clipboard),
Toast.LENGTH_SHORT
).show()
}
}
},
shape = RoundedCornerShape(20.dp), colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary
)
) {
Icon(
tint = Color.White,
imageVector = Icons.Default.Key,
contentDescription = stringResource(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup)
)
Text("Copy Secret Key", color = MaterialTheme.colors.onPrimary)
}
}

View File

@@ -10,7 +10,6 @@ import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.filled.EditNote import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material.icons.filled.Key
import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.Link
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Share
@@ -78,7 +77,6 @@ import com.vitorpamplona.amethyst.ui.screen.UserFeedView
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import nostr.postr.toNsec
@OptIn(ExperimentalPagerApi::class) @OptIn(ExperimentalPagerApi::class)
@Composable @Composable
@@ -337,10 +335,6 @@ private fun ProfileHeader(
.padding(bottom = 3.dp)) { .padding(bottom = 3.dp)) {
MessageButton(baseUser, navController) MessageButton(baseUser, navController)
if (accountUser == baseUser && account.isWriteable()) {
NSecCopyButton(account)
}
NPubCopyButton(baseUser) NPubCopyButton(baseUser)
if (accountUser == baseUser) { if (accountUser == baseUser) {
@@ -637,40 +631,7 @@ fun TabRelays(user: User, accountViewModel: AccountViewModel, navController: Nav
} }
} }
@Composable
private fun NSecCopyButton(
account: Account
) {
val clipboardManager = LocalClipboardManager.current
var popupExpanded by remember { mutableStateOf(false) }
Button(
modifier = Modifier
.padding(horizontal = 3.dp)
.width(50.dp),
onClick = { popupExpanded = true },
shape = RoundedCornerShape(20.dp),
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
) {
Icon(
tint = Color.White,
imageVector = Icons.Default.Key,
contentDescription = stringResource(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup)
)
DropdownMenu(
expanded = popupExpanded,
onDismissRequest = { popupExpanded = false }
) {
DropdownMenuItem(onClick = { account.loggedIn.privKey?.let { clipboardManager.setText(AnnotatedString(it.toNsec())) }; popupExpanded = false }) {
Text(stringResource(R.string.copy_private_key_to_the_clipboard))
}
}
}
}
@Composable @Composable
private fun NPubCopyButton( private fun NPubCopyButton(

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333"
android:alpha="0.6">
<path
android:fillColor="@android:color/white"
android:pathData="M21,10h-8.35C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H13l2,2l2,-2l2,2l4,-4.04L21,10zM7,15c-1.65,0 -3,-1.35 -3,-3c0,-1.65 1.35,-3 3,-3s3,1.35 3,3C10,13.65 8.65,15 7,15z"/>
</vector>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333"
android:alpha="0.6"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M17,7l-1.41,1.41L18.17,11H8v2h10.17l-2.58,2.58L17,17l5,-5zM4,5h8V3H4c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h8v-2H4V5z"/>
</vector>

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333"
android:alpha="0.6">
<path
android:fillColor="@android:color/white"
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12L21,5l-9,-4zM12,11.99h7c-0.53,4.12 -3.28,7.79 -7,8.94L12,12L5,12L5,6.3l7,-3.11v8.8z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 991 B

View File

@@ -118,7 +118,7 @@
<string name="website">Website</string> <string name="website">Website</string>
<string name="lightning_address">Lightning Address</string> <string name="lightning_address">Lightning Address</string>
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">Copies the Nsec ID (your password) to the clipboard for backup</string> <string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">Copies the Nsec ID (your password) to the clipboard for backup</string>
<string name="copy_private_key_to_the_clipboard">Copy Private Key to the Clipboard</string> <string name="copy_private_key_to_the_clipboard">Copy Secret Key to the Clipboard</string>
<string name="copies_the_public_key_to_the_clipboard_for_sharing">Copies the public key to the clipboard for sharing</string> <string name="copies_the_public_key_to_the_clipboard_for_sharing">Copies the public key to the clipboard for sharing</string>
<string name="copy_public_key_npub_to_the_clipboard">Copy Public Key (NPub) to the Clipboard</string> <string name="copy_public_key_npub_to_the_clipboard">Copy Public Key (NPub) to the Clipboard</string>
<string name="send_a_direct_message">Send a Direct Message</string> <string name="send_a_direct_message">Send a Direct Message</string>
@@ -177,4 +177,12 @@
<string name="report_hateful_speech">Report Hateful speech</string> <string name="report_hateful_speech">Report Hateful speech</string>
<string name="report_nudity_porn">Report Nudity / Porn</string> <string name="report_nudity_porn">Report Nudity / Porn</string>
<string name="others">others</string> <string name="others">others</string>
<string name="account_backup_tips_md">
## Key Backup and Safety Tips
\n\nYour account is secured by a secret key. The key is long random string starting with **nsec1**. Anyone who has access to your secret key can publish content using your identity.
\n\n- Do **not** put your secret key in any website or software you do not trust.
\n- Amethyst developers will **never** ask you for your secret key.
\n- **Do** keep a secure backup of your secret key for account recovery. We recommend using a password manager.
</string>
<string name="secret_key_copied_to_clipboard">Secret key (nsec) copied to clipboard</string>
</resources> </resources>