Merge pull request #182 from maxmoney21m/feature/backup-keys

Move nsec backup to drawer and organize drawer
This commit is contained in:
Vitor Pamplona 2023-03-03 16:08:42 -05:00 committed by GitHub
commit d3b557b778
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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.Column
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Divider
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.platform.LocalContext
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.Companion.W500
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
@ -45,14 +46,14 @@ import androidx.navigation.NavHostController
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.RoboHashCache
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.launch
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.platform.LocalContext
@Composable
fun DrawerContent(navController: NavHostController,
@ -88,7 +89,8 @@ fun DrawerContent(navController: NavHostController,
modifier = Modifier
.fillMaxWidth()
.weight(1F),
accountStateViewModel
accountStateViewModel,
account,
)
BottomContent(account.userProfile(), scaffoldState, navController)
@ -155,7 +157,9 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol
if (accountUser.bestDisplayName() != null) {
Text(
accountUser.bestDisplayName() ?: "",
modifier = Modifier.padding(top = 7.dp).clickable(onClick = {
modifier = Modifier
.padding(top = 7.dp)
.clickable(onClick = {
accountUser.let {
navController.navigate("User/${it.pubkeyHex}")
}
@ -169,7 +173,9 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol
}
if (accountUser.bestUsername() != null) {
Text(" @${accountUser.bestUsername()}", color = Color.LightGray,
modifier = Modifier.padding(top = 15.dp).clickable(onClick = {
modifier = Modifier
.padding(top = 15.dp)
.clickable(onClick = {
accountUser.let {
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 {
navController.navigate("User/${it.pubkeyHex}")
}
@ -206,59 +214,70 @@ fun ListContent(
navController: NavHostController,
scaffoldState: ScaffoldState,
modifier: Modifier,
accountViewModel: AccountStateViewModel
accountViewModel: AccountStateViewModel,
account: Account,
) {
val coroutineScope = rememberCoroutineScope()
var backupDialogOpen by remember { mutableStateOf(false) }
Column(modifier = modifier) {
LazyColumn() {
item {
Column(modifier = modifier.fillMaxHeight()) {
if (accountUser != null)
NavigationRow(navController,
scaffoldState,
"User/${accountUser.pubkeyHex}",
Route.Profile.icon,
stringResource(R.string.profile)
NavigationRow(
title = stringResource(R.string.profile),
icon = Route.Profile.icon,
tint = MaterialTheme.colors.primary,
navController = navController,
scaffoldState = scaffoldState,
route = "User/${accountUser.pubkeyHex}",
)
Divider(
modifier = Modifier.padding(bottom = 15.dp),
thickness = 0.25.dp
Divider(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 = {
navController.navigate(Route.Filters.route)
coroutineScope.launch {
scaffoldState.drawerState.close()
}
})) {
Text(
text = stringResource(R.string.security_filters),
fontSize = 18.sp,
fontWeight = W500
Divider(thickness = 0.25.dp)
IconRow(
title = "Backup Keys",
icon = R.drawable.ic_key,
tint = MaterialTheme.colors.onBackground,
onClick = { backupDialogOpen = true }
)
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(
text = stringResource(R.string.log_out),
modifier = Modifier.padding(vertical = 15.dp),
fontSize = 18.sp,
fontWeight = W500
)
}
}
}
}
if (backupDialogOpen) {
AccountBackupDialog(account, onClose = { backupDialogOpen = false })
}
}
@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 currentRoute = currentRoute(navController)
Row(modifier = Modifier
.fillMaxWidth()
.clickable(onClick = {
IconRow(title, icon, tint, onClick = {
if (currentRoute != route) {
navController.navigate(route)
}
@ -266,6 +285,13 @@ fun NavigationRow(navController: NavHostController, scaffoldState: ScaffoldState
scaffoldState.drawerState.close()
}
})
}
@Composable
fun IconRow(title: String, icon: Int, tint: Color, onClick: () -> Unit) {
Row(modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
) {
Row(
modifier = Modifier
@ -276,7 +302,7 @@ fun NavigationRow(navController: NavHostController, scaffoldState: ScaffoldState
Icon(
painter = painterResource(icon), null,
modifier = Modifier.size(22.dp),
tint = MaterialTheme.colors.primary
tint = tint
)
Text(
modifier = Modifier.padding(start = 16.dp),

View File

@ -53,7 +53,7 @@ sealed class Route(
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) }}
)

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.filled.Bolt
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.MoreVert
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import nostr.postr.toNsec
@OptIn(ExperimentalPagerApi::class)
@Composable
@ -337,10 +335,6 @@ private fun ProfileHeader(
.padding(bottom = 3.dp)) {
MessageButton(baseUser, navController)
if (accountUser == baseUser && account.isWriteable()) {
NSecCopyButton(account)
}
NPubCopyButton(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
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

@ -113,7 +113,7 @@
<string name="website">Website</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="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="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>
@ -172,4 +172,12 @@
<string name="report_hateful_speech">Report Hateful speech</string>
<string name="report_nudity_porn">Report Nudity / Porn</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>