diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index acdc717d0..5964847c8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -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,38 +157,44 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol if (accountUser.bestDisplayName() != null) { Text( accountUser.bestDisplayName() ?: "", - modifier = Modifier.padding(top = 7.dp).clickable(onClick = { - accountUser.let { - navController.navigate("User/${it.pubkeyHex}") - } - coroutineScope.launch { - scaffoldState.drawerState.close() - } - }), + modifier = Modifier + .padding(top = 7.dp) + .clickable(onClick = { + accountUser.let { + navController.navigate("User/${it.pubkeyHex}") + } + coroutineScope.launch { + scaffoldState.drawerState.close() + } + }), fontWeight = FontWeight.Bold, fontSize = 18.sp ) } if (accountUser.bestUsername() != null) { Text(" @${accountUser.bestUsername()}", color = Color.LightGray, - modifier = Modifier.padding(top = 15.dp).clickable(onClick = { - accountUser.let { - navController.navigate("User/${it.pubkeyHex}") - } - coroutineScope.launch { - scaffoldState.drawerState.close() - } - }) + modifier = Modifier + .padding(top = 15.dp) + .clickable(onClick = { + accountUser.let { + navController.navigate("User/${it.pubkeyHex}") + } + coroutineScope.launch { + scaffoldState.drawerState.close() + } + }) ) } - Row(modifier = Modifier.padding(top = 15.dp).clickable(onClick = { - accountUser.let { - navController.navigate("User/${it.pubkeyHex}") - } - coroutineScope.launch { - scaffoldState.drawerState.close() - } - })) { + Row(modifier = Modifier + .padding(top = 15.dp) + .clickable(onClick = { + accountUser.let { + navController.navigate("User/${it.pubkeyHex}") + } + coroutineScope.launch { + scaffoldState.drawerState.close() + } + })) { Row() { Text("${accountUserFollows.follows.size}", fontWeight = FontWeight.Bold) Text(stringResource(R.string.following)) @@ -206,66 +214,84 @@ 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 { - if (accountUser != null) - NavigationRow(navController, - scaffoldState, - "User/${accountUser.pubkeyHex}", - Route.Profile.icon, - stringResource(R.string.profile) - ) + Column(modifier = modifier.fillMaxHeight()) { + if (accountUser != null) + 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 - ) - 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 - ) - } - 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 - ) - } - } - } - } + 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, + ) + + 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() } + ) + } + + 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) + IconRow(title, icon, tint, onClick = { + if (currentRoute != route) { + navController.navigate(route) + } + coroutineScope.launch { + scaffoldState.drawerState.close() + } + }) +} + +@Composable +fun IconRow(title: String, icon: Int, tint: Color, onClick: () -> Unit) { Row(modifier = Modifier .fillMaxWidth() - .clickable(onClick = { - if (currentRoute != route) { - navController.navigate(route) - } - coroutineScope.launch { - scaffoldState.drawerState.close() - } - }) + .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), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index bf48cecf0..2df1fee46 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -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) }} ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt new file mode 100644 index 000000000..e56431e08 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt @@ -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) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index b16677610..df52949e4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -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( diff --git a/app/src/main/res/drawable-anydpi/ic_key.xml b/app/src/main/res/drawable-anydpi/ic_key.xml new file mode 100644 index 000000000..0db74fa0d --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_key.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_logout.xml b/app/src/main/res/drawable-anydpi/ic_logout.xml new file mode 100644 index 000000000..6555606de --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_logout.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_security.xml b/app/src/main/res/drawable-anydpi/ic_security.xml new file mode 100644 index 000000000..717b89c05 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_security.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable-hdpi/ic_key.png b/app/src/main/res/drawable-hdpi/ic_key.png new file mode 100644 index 000000000..81ef03db8 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_key.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_logout.png b/app/src/main/res/drawable-hdpi/ic_logout.png new file mode 100644 index 000000000..a74a0f2a1 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_logout.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_security.png b/app/src/main/res/drawable-hdpi/ic_security.png new file mode 100644 index 000000000..5d563a786 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_security.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_key.png b/app/src/main/res/drawable-mdpi/ic_key.png new file mode 100644 index 000000000..67341e7e1 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_key.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_logout.png b/app/src/main/res/drawable-mdpi/ic_logout.png new file mode 100644 index 000000000..dc2dabde5 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_logout.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_security.png b/app/src/main/res/drawable-mdpi/ic_security.png new file mode 100644 index 000000000..3dc8abcce Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_security.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_key.png b/app/src/main/res/drawable-xhdpi/ic_key.png new file mode 100644 index 000000000..79ffc2e67 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_key.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_logout.png b/app/src/main/res/drawable-xhdpi/ic_logout.png new file mode 100644 index 000000000..10fd5751c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_logout.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_security.png b/app/src/main/res/drawable-xhdpi/ic_security.png new file mode 100644 index 000000000..d8f6c01f2 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_security.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_key.png b/app/src/main/res/drawable-xxhdpi/ic_key.png new file mode 100644 index 000000000..e0cdfef0a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_key.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_logout.png b/app/src/main/res/drawable-xxhdpi/ic_logout.png new file mode 100644 index 000000000..c19029b20 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_logout.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_security.png b/app/src/main/res/drawable-xxhdpi/ic_security.png new file mode 100644 index 000000000..dba255c73 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_security.png differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1d6d7acef..0b98f9b90 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -113,7 +113,7 @@ Website Lightning Address Copies the Nsec ID (your password) to the clipboard for backup - Copy Private Key to the Clipboard + Copy Secret Key to the Clipboard Copies the public key to the clipboard for sharing Copy Public Key (NPub) to the Clipboard Send a Direct Message @@ -172,4 +172,12 @@ Report Hateful speech Report Nudity / Porn others + + ## 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. + + Secret key (nsec) copied to clipboard \ No newline at end of file