Merge pull request #226 from maxmoney21m/feature/71-note-menu

Redesign note long click menu, URL sharing
pull/228/head
Vitor Pamplona 2023-03-08 13:15:13 -05:00 zatwierdzone przez GitHub
commit b62adc61ab
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
8 zmienionych plików z 362 dodań i 16 usunięć

Wyświetl plik

@ -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<String, String>()
}
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 {

Wyświetl plik

@ -56,6 +56,7 @@ class Account(
var languagePreferences: Map<String, String> = mapOf(),
var translateTo: String = Locale.getDefault().language,
var zapAmountChoices: List<Long> = listOf(500L, 1000L, 5000L),
var hideDeleteRequestInfo: Boolean = false,
var backupContactList: ContactListEvent? = null
) {
var transientHiddenUsers: Set<String> = setOf()
@ -540,6 +541,11 @@ class Account(
saveable.invalidateData()
}
fun setHideDeleteRequestInfo() {
hideDeleteRequestInfo = true
saveable.invalidateData()
}
init {
backupContactList?.let {
println("Loading saved contacts ${it.toJson()}")

Wyświetl plik

@ -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)
}
}
}
}
}
}

Wyświetl plik

@ -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))
}

Wyświetl plik

@ -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)
}
}

Wyświetl plik

@ -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()
}
}

Wyświetl plik

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="48dp"
android:viewportHeight="960" android:viewportWidth="960"
android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M439,840v-60h83v60h-83ZM439,180v-60h83v60h-83ZM609,840v-60h83v60h-83ZM609,180v-60h83v60h-83ZM780,840v-60h60v60h-60ZM780,180v-60h60v60h-60ZM120,840v-60h86L206,180h-86v-60h231v60h-85v600h85v60L120,840ZM694,626 L652,584 725,510L414,510v-60h311l-73,-74 42,-42 146,146 -146,146Z"/>
</vector>

Wyświetl plik

@ -1,4 +1,4 @@
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name_release" translatable="false">Amethyst</string>
<string name="app_name_debug" translatable="false">Amethyst Debug</string>
<string name="point_to_the_qr_code">Point to the QR Code</string>
@ -20,7 +20,7 @@
<string name="relay_icon">Relay Icon</string>
<string name="unknown_author">Unknown Author</string>
<string name="copy_text">Copy Text</string>
<string name="copy_user_pubkey">Copy User PubKey</string>
<string name="copy_user_pubkey">Copy Author @npub</string>
<string name="copy_note_id">Copy Note ID</string>
<string name="broadcast">Broadcast</string>
<string name="block_hide_user"><![CDATA[Block & Hide User]]></string>
@ -177,7 +177,7 @@
<string name="mark_all_known_as_read">Mark all Known as read</string>
<string name="mark_all_new_as_read">Mark all New as read</string>
<string name="mark_all_as_read">Mark all as read</string>
<string name="account_backup_tips_md">
<string name="account_backup_tips_md" tools:ignore="Typos">
## 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 @@
<string name="badge_award_image_for">"Badge award image for %1$s"</string>
<string name="new_badge_award_notif">You Received a new Badge Award</string>
<string name="award_granted_to">Badge award granted to</string>
</resources>
<string name="copied_note_text_to_clipboard">Copied note text to clipboard</string>
<string name="copied_user_id_to_clipboard" tools:ignore="Typos">Copied authors @npub to clipboard</string>
<string name="copied_note_id_to_clipboard" tools:ignore="Typos">Copied note ID (@note1) to clipboard</string>
<string name="select_text_dialog_top">Select Text</string>
<string name="quick_action_select">Select</string>
<string name="quick_action_share_browser_link">Share Browser Link</string>
<string name="quick_action_share">Share</string>
<string name="quick_action_copy_user_id">Mention</string>
<string name="quick_action_copy_note_id">Quote</string>
<string name="quick_action_copy_text">Copy</string>
<string name="quick_action_delete">Delete</string>
<string name="quick_action_unfollow">Unfollow</string>
<string name="quick_action_follow">Follow</string>
<string name="quick_action_request_deletion_alert_title">Request Deletion</string>
<string name="quick_action_request_deletion_alert_body">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.</string>
</resources>