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