kopia lustrzana https://github.com/vitorpamplona/amethyst
Merge pull request #226 from maxmoney21m/feature/71-note-menu
Redesign note long click menu, URL sharingpull/228/head
commit
b62adc61ab
|
@ -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 {
|
||||
|
|
|
@ -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()}")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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 author’s @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>
|
||||
|
|
Ładowanie…
Reference in New Issue