diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index f0dfe6331..abec38a63 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -25,6 +25,7 @@ class LocalPreferences(context: Context) { const val TRANSLATE_TO = "translateTo" const val ZAP_AMOUNTS = "zapAmounts" const val LATEST_CONTACT_LIST = "latestContactList" + const val HIDE_DELETE_REQUEST_INFO = "hideDeleteRequestInfo" val LAST_READ: (String) -> String = { route -> "last_read_route_$route" } } @@ -49,6 +50,7 @@ class LocalPreferences(context: Context) { account.translateTo.let { putString(PrefKeys.TRANSLATE_TO, it) } account.zapAmountChoices.let { putString(PrefKeys.ZAP_AMOUNTS, gson.toJson(it)) } account.backupContactList.let { putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(it)) } + putBoolean(PrefKeys.HIDE_DELETE_REQUEST_INFO, account.hideDeleteRequestInfo) }.apply() } @@ -89,6 +91,8 @@ class LocalPreferences(context: Context) { mapOf() } + val hideDeleteRequestInfo = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_INFO, false) + if (pubKey != null) { return Account( Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()), @@ -99,6 +103,7 @@ class LocalPreferences(context: Context) { languagePreferences, translateTo, zapAmountChoices, + hideDeleteRequestInfo, latestContactList ) } else { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index ebafe6b5c..92a9a7f1c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -56,6 +56,7 @@ class Account( var languagePreferences: Map = mapOf(), var translateTo: String = Locale.getDefault().language, var zapAmountChoices: List = listOf(500L, 1000L, 5000L), + var hideDeleteRequestInfo: Boolean = false, var backupContactList: ContactListEvent? = null ) { var transientHiddenUsers: Set = setOf() @@ -540,6 +541,11 @@ class Account( saveable.invalidateData() } + fun setHideDeleteRequestInfo() { + hideDeleteRequestInfo = true + saveable.invalidateData() + } + init { backupContactList?.let { println("Loading saved contacts ${it.toJson()}") diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SelectTextDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SelectTextDialog.kt new file mode 100644 index 000000000..bd9d488b9 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SelectTextDialog.kt @@ -0,0 +1,57 @@ +package com.vitorpamplona.amethyst.ui.components + +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.padding +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.Card +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.vitorpamplona.amethyst.R + +@Composable +fun SelectTextDialog(text: String, onDismiss: () -> Unit) { + Dialog( + onDismissRequest = onDismiss + ) { + Card { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + IconButton( + onClick = onDismiss, + modifier = Modifier.background(MaterialTheme.colors.background) + ) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = null, + tint = MaterialTheme.colors.primary + ) + } + Text(text = stringResource(R.string.select_text_dialog_top)) + } + Divider() + Row(modifier = Modifier.padding(16.dp)) { + SelectionContainer { + Text(text) + } + } + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 4da4d92db..045887019 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -431,7 +430,7 @@ fun NoteCompose( ) } - NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) + NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) } } } @@ -756,16 +755,6 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, } Divider() } - DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(accountViewModel.decrypt(note) ?: "")); onDismiss() }) { - Text(stringResource(R.string.copy_text)) - } - DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(note.author?.pubkeyNpub() ?: "")); onDismiss() }) { - Text(stringResource(R.string.copy_user_pubkey)) - } - DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(note.idNote())); onDismiss() }) { - Text(stringResource(R.string.copy_note_id)) - } - Divider() DropdownMenuItem(onClick = { accountViewModel.broadcast(note); onDismiss() }) { Text(stringResource(R.string.broadcast)) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt new file mode 100644 index 000000000..bf2d910d2 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -0,0 +1,253 @@ +package com.vitorpamplona.amethyst.ui.note + +import android.content.Intent +import android.widget.Toast +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +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.shape.RoundedCornerShape +import androidx.compose.material.AlertDialog +import androidx.compose.material.Button +import androidx.compose.material.Card +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AlternateEmail +import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.FormatQuote +import androidx.compose.material.icons.filled.PersonAdd +import androidx.compose.material.icons.filled.PersonRemove +import androidx.compose.material.icons.filled.Share +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Popup +import androidx.core.content.ContextCompat +import androidx.core.graphics.ColorUtils +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.SelectTextDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import kotlinx.coroutines.launch + +fun lightenColor(color: Color, amount: Float): Color { + var argb = color.toArgb() + val hslOut = floatArrayOf(0f, 0f, 0f) + ColorUtils.colorToHSL(argb, hslOut) + hslOut[2] += amount + argb = ColorUtils.HSLToColor(hslOut) + return Color(argb) +} + +val externalLinkForNote = { note: Note -> "https://snort.social/e/${note.idNote()}" } + +@Composable +fun VerticalDivider(color: Color) = + Divider( + color = color, + modifier = Modifier + .fillMaxHeight() + .width(1.dp) + ) + +@Composable +fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, accountViewModel: AccountViewModel) { + val context = LocalContext.current + val primaryLight = lightenColor(MaterialTheme.colors.primary, 0.2f) + val cardShape = RoundedCornerShape(5.dp) + val clipboardManager = LocalClipboardManager.current + val scope = rememberCoroutineScope() + var showSelectTextDialog by remember { mutableStateOf(false) } + var showDeleteAlertDialog by remember { mutableStateOf(false) } + val isOwnNote = note.author == accountViewModel.userProfile() + val isFollowingUser = !isOwnNote && accountViewModel.isFollowing(note.author!!) + + val showToast = { stringResource: Int -> + scope.launch { + Toast.makeText( + context, + context.getString(stringResource), + Toast.LENGTH_SHORT + ).show() + } + } + + if (popupExpanded) { + Popup(onDismissRequest = onDismiss) { + Card( + modifier = Modifier.shadow(elevation = 6.dp, shape = cardShape), + shape = cardShape, + backgroundColor = MaterialTheme.colors.primary + ) { + Column(modifier = Modifier.width(IntrinsicSize.Min)) { + Row(modifier = Modifier.height(IntrinsicSize.Min)) { + NoteQuickActionItem( + icon = Icons.Default.ContentCopy, + label = stringResource(R.string.quick_action_copy_text) + ) { + clipboardManager.setText( + AnnotatedString( + accountViewModel.decrypt(note) ?: "" + ) + ) + showToast(R.string.copied_note_text_to_clipboard) + onDismiss() + } + VerticalDivider(primaryLight) + NoteQuickActionItem(Icons.Default.AlternateEmail, stringResource(R.string.quick_action_copy_user_id)) { + clipboardManager.setText(AnnotatedString("@${note.author?.pubkeyNpub()}" ?: "")) + showToast(R.string.copied_user_id_to_clipboard) + onDismiss() + } + VerticalDivider(primaryLight) + NoteQuickActionItem(Icons.Default.FormatQuote, stringResource(R.string.quick_action_copy_note_id)) { + clipboardManager.setText(AnnotatedString("@${note.idNote()}")) + showToast(R.string.copied_note_id_to_clipboard) + onDismiss() + } + } + Divider( + color = primaryLight, + modifier = Modifier + .fillMaxWidth() + .width(1.dp) + ) + Row(modifier = Modifier.height(IntrinsicSize.Min)) { + if (isOwnNote) { + NoteQuickActionItem(Icons.Default.Delete, stringResource(R.string.quick_action_delete)) { + if (accountViewModel.hideDeleteRequestInfo()) { + accountViewModel.delete(note) + onDismiss() + } else { + showDeleteAlertDialog = true + } + } + } else if (isFollowingUser) { + NoteQuickActionItem(Icons.Default.PersonRemove, stringResource(R.string.quick_action_unfollow)) { + accountViewModel.unfollow(note.author!!) + onDismiss() + } + } else { + NoteQuickActionItem(Icons.Default.PersonAdd, stringResource(R.string.quick_action_follow)) { + accountViewModel.follow(note.author!!) + onDismiss() + } + } + + VerticalDivider(primaryLight) + NoteQuickActionItem( + icon = ImageVector.vectorResource(id = R.drawable.text_select_move_forward_character), + label = stringResource(R.string.quick_action_select) + ) { + showSelectTextDialog = true + onDismiss() + } + VerticalDivider(primaryLight) + NoteQuickActionItem(icon = Icons.Default.Share, label = stringResource(R.string.quick_action_share)) { + val sendIntent = Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra( + Intent.EXTRA_TEXT, + externalLinkForNote(note) + ) + putExtra(Intent.EXTRA_TITLE, context.getString(R.string.quick_action_share_browser_link)) + } + + val shareIntent = Intent.createChooser(sendIntent, context.getString(R.string.quick_action_share)) + ContextCompat.startActivity(context, shareIntent, null) + onDismiss() + } + VerticalDivider(primaryLight) + } + } + } + } + } + + if (showSelectTextDialog) { + accountViewModel.decrypt(note)?.let { + SelectTextDialog(it) { showSelectTextDialog = false } + } + } + + if (showDeleteAlertDialog) { + AlertDialog( + onDismissRequest = { onDismiss() }, + title = { + Text(text = stringResource(R.string.quick_action_request_deletion_alert_title)) + }, + text = { + Text(text = stringResource(R.string.quick_action_request_deletion_alert_body)) + }, + buttons = { + Row( + modifier = Modifier.padding(all = 8.dp).fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + TextButton( + onClick = { + accountViewModel.setHideDeleteRequestInfo() + accountViewModel.delete(note) + onDismiss() + } + ) { + Text("Don't show again") + } + Button( + onClick = { accountViewModel.delete(note); onDismiss() } + ) { + Text("Delete") + } + } + } + ) + } +} + +@Composable +fun NoteQuickActionItem(icon: ImageVector, label: String, onClick: () -> Unit) { + Column( + modifier = Modifier + .size(64.dp) + .clickable { onClick() }, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colors.onPrimary + ) + Text(text = label, fontSize = 12.sp) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 2b36d2fbb..2734c6ee1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -120,4 +120,20 @@ class AccountViewModel(private val account: Account) : ViewModel() { fun follow(user: User) { account.follow(user) } + + fun unfollow(user: User) { + account.unfollow(user) + } + + fun isFollowing(user: User): Boolean { + return account.userProfile().isFollowing(user) + } + + fun hideDeleteRequestInfo(): Boolean { + return account.hideDeleteRequestInfo + } + + fun setHideDeleteRequestInfo() { + account.setHideDeleteRequestInfo() + } } diff --git a/app/src/main/res/drawable/text_select_move_forward_character.xml b/app/src/main/res/drawable/text_select_move_forward_character.xml new file mode 100644 index 000000000..312d40dbc --- /dev/null +++ b/app/src/main/res/drawable/text_select_move_forward_character.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eb90867a7..bf1bebabe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,4 @@ - + Amethyst Amethyst Debug Point to the QR Code @@ -20,7 +20,7 @@ Relay Icon Unknown Author Copy Text - Copy User PubKey + Copy Author @npub Copy Note ID Broadcast @@ -177,7 +177,7 @@ Mark all Known as read Mark all New as read Mark all as read - + ## 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. @@ -192,4 +192,19 @@ "Badge award image for %1$s" You Received a new Badge Award Badge award granted to - \ No newline at end of file + Copied note text to clipboard + Copied author’s @npub to clipboard + Copied note ID (@note1) to clipboard + Select Text + Select + Share Browser Link + Share + Mention + Quote + Copy + Delete + Unfollow + Follow + Request Deletion + Amethyst will request that your note be deleted from the relays you are currently connected to. There is no guarantee that your note will be permanently deleted from those relays, or from other relays where it may be stored. +