Merge pull request #275 from maxmoney21m/feature/216-merge-reports

Add separate screen for report & block
This commit is contained in:
Vitor Pamplona
2023-03-15 08:09:07 -04:00
committed by GitHub
12 changed files with 506 additions and 115 deletions
+1
View File
@@ -68,6 +68,7 @@ render.experimental.xml
.idea/**/caches/ .idea/**/caches/
.idea/**/libraries/ .idea/**/libraries/
.idea/**/shelf/ .idea/**/shelf/
.idea/**/codeStyles
.idea/**/workspace.xml .idea/**/workspace.xml
.idea/**/tasks.xml .idea/**/tasks.xml
.idea/**/.name .idea/**/.name
@@ -47,7 +47,8 @@ private object PrefKeys {
const val TRANSLATE_TO = "translateTo" const val TRANSLATE_TO = "translateTo"
const val ZAP_AMOUNTS = "zapAmounts" const val ZAP_AMOUNTS = "zapAmounts"
const val LATEST_CONTACT_LIST = "latestContactList" const val LATEST_CONTACT_LIST = "latestContactList"
const val HIDE_DELETE_REQUEST_INFO = "hideDeleteRequestInfo" const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog"
const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog"
val LAST_READ: (String) -> String = { route -> "last_read_route_$route" } val LAST_READ: (String) -> String = { route -> "last_read_route_$route" }
} }
@@ -183,7 +184,8 @@ object LocalPreferences {
putString(PrefKeys.TRANSLATE_TO, account.translateTo) putString(PrefKeys.TRANSLATE_TO, account.translateTo)
putString(PrefKeys.ZAP_AMOUNTS, gson.toJson(account.zapAmountChoices)) putString(PrefKeys.ZAP_AMOUNTS, gson.toJson(account.zapAmountChoices))
putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList)) putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList))
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_INFO, account.hideDeleteRequestInfo) putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog)
putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog)
putString(PrefKeys.DISPLAY_NAME, account.userProfile().toBestDisplayName()) putString(PrefKeys.DISPLAY_NAME, account.userProfile().toBestDisplayName())
putString(PrefKeys.PROFILE_PICTURE_URL, account.userProfile().profilePicture()) putString(PrefKeys.PROFILE_PICTURE_URL, account.userProfile().profilePicture())
}.apply() }.apply()
@@ -230,7 +232,8 @@ object LocalPreferences {
mapOf() mapOf()
} }
val hideDeleteRequestInfo = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_INFO, false) val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false)
val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false)
return Account( return Account(
Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()), Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()),
@@ -241,7 +244,8 @@ object LocalPreferences {
languagePreferences, languagePreferences,
translateTo, translateTo,
zapAmountChoices, zapAmountChoices,
hideDeleteRequestInfo, hideDeleteRequestDialog,
hideBlockAlertDialog,
latestContactList latestContactList
) )
} }
@@ -289,8 +293,8 @@ object LocalPreferences {
stringPrefs.forEach { userPrefs.putString(it, appPrefs.getString(it, null)) } stringPrefs.forEach { userPrefs.putString(it, appPrefs.getString(it, null)) }
stringSetPrefs.forEach { userPrefs.putStringSet(it, appPrefs.getStringSet(it, null)) } stringSetPrefs.forEach { userPrefs.putStringSet(it, appPrefs.getStringSet(it, null)) }
userPrefs.putBoolean( userPrefs.putBoolean(
PrefKeys.HIDE_DELETE_REQUEST_INFO, PrefKeys.HIDE_DELETE_REQUEST_DIALOG,
appPrefs.getBoolean(PrefKeys.HIDE_DELETE_REQUEST_INFO, false) appPrefs.getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false)
) )
}.apply() }.apply()
} }
@@ -59,7 +59,8 @@ class Account(
var languagePreferences: Map<String, String> = mapOf(), var languagePreferences: Map<String, String> = mapOf(),
var translateTo: String = Locale.getDefault().language, var translateTo: String = Locale.getDefault().language,
var zapAmountChoices: List<Long> = listOf(500L, 1000L, 5000L), var zapAmountChoices: List<Long> = listOf(500L, 1000L, 5000L),
var hideDeleteRequestInfo: Boolean = false, var hideDeleteRequestDialog: Boolean = false,
var hideBlockAlertDialog: Boolean = false,
var backupContactList: ContactListEvent? = null var backupContactList: ContactListEvent? = null
) { ) {
var transientHiddenUsers: Set<String> = setOf() var transientHiddenUsers: Set<String> = setOf()
@@ -170,7 +171,7 @@ class Account(
return LnZapRequestEvent.create(userPubKeyHex, userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!) return LnZapRequestEvent.create(userPubKeyHex, userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!)
} }
fun report(note: Note, type: ReportEvent.ReportType) { fun report(note: Note, type: ReportEvent.ReportType, content: String = "") {
if (!isWriteable()) return if (!isWriteable()) return
if (note.hasReacted(userProfile(), "⚠️")) { if (note.hasReacted(userProfile(), "⚠️")) {
@@ -185,7 +186,7 @@ class Account(
} }
note.event?.let { note.event?.let {
val event = ReportEvent.create(it, type, loggedIn.privKey!!) val event = ReportEvent.create(it, type, loggedIn.privKey!!, content = content)
Client.send(event) Client.send(event)
LocalCache.consume(event, null) LocalCache.consume(event, null)
} }
@@ -553,8 +554,13 @@ class Account(
saveable.invalidateData() saveable.invalidateData()
} }
fun setHideDeleteRequestInfo() { fun setHideDeleteRequestDialog() {
hideDeleteRequestInfo = true hideDeleteRequestDialog = true
saveable.invalidateData()
}
fun setHideBlockAlertDialog() {
hideBlockAlertDialog = true
saveable.invalidateData() saveable.invalidateData()
} }
@@ -57,9 +57,13 @@ class ReportEvent(
companion object { companion object {
const val kind = 1984 const val kind = 1984
fun create(reportedPost: EventInterface, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent { fun create(
val content = "" reportedPost: EventInterface,
type: ReportType,
privateKey: ByteArray,
content: String = "",
createdAt: Long = Date().time / 1000
): ReportEvent {
val reportPostTag = listOf("e", reportedPost.id(), type.name.lowercase()) val reportPostTag = listOf("e", reportedPost.id(), type.name.lowercase())
val reportAuthorTag = listOf("p", reportedPost.pubKey(), type.name.lowercase()) val reportAuthorTag = listOf("p", reportedPost.pubKey(), type.name.lowercase())
@@ -0,0 +1,101 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
@Composable
fun TextSpinner(label: String, placeholder: String, options: List<String>, onSelect: (Int) -> Unit, modifier: Modifier = Modifier) {
val focusRequester = remember { FocusRequester() }
val interactionSource = remember { MutableInteractionSource() }
var optionsShowing by remember { mutableStateOf(false) }
var currentText by remember { mutableStateOf(placeholder) }
Box(
modifier = modifier
) {
OutlinedTextField(
value = currentText,
onValueChange = {},
readOnly = true,
label = { Text(label) },
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester)
)
Box(
modifier = Modifier
.matchParentSize()
.clickable(
interactionSource = interactionSource,
indication = null
) {
optionsShowing = true
focusRequester.requestFocus()
}
)
}
if (optionsShowing) {
options.isNotEmpty().also {
SpinnerSelectionDialog(options = options, onDismiss = { optionsShowing = false }) {
currentText = options[it]
optionsShowing = false
onSelect(it)
}
}
}
}
@Composable
fun SpinnerSelectionDialog(options: List<String>, onDismiss: () -> Unit, onSelect: (Int) -> Unit) {
Dialog(onDismissRequest = onDismiss) {
Surface(
border = BorderStroke(0.25.dp, Color.LightGray),
shape = RoundedCornerShape(5.dp)
) {
LazyColumn() {
itemsIndexed(options) { index, item ->
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp, 16.dp)
.clickable {
onSelect(index)
}
) {
Text(text = item, color = MaterialTheme.colors.onSurface)
}
if (index < options.lastIndex) {
Divider(color = Color.LightGray, thickness = 0.25.dp)
}
}
}
}
}
}
@@ -344,7 +344,7 @@ fun ChatroomMessageCompose(
} }
} }
NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
} }
} }
} }
@@ -59,6 +59,7 @@ import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
import com.vitorpamplona.amethyst.ui.theme.Following import com.vitorpamplona.amethyst.ui.theme.Following
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -779,6 +780,7 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
val clipboardManager = LocalClipboardManager.current val clipboardManager = LocalClipboardManager.current
val appContext = LocalContext.current.applicationContext val appContext = LocalContext.current.applicationContext
val actContext = LocalContext.current val actContext = LocalContext.current
var reportDialogShowing by remember { mutableStateOf(false) }
DropdownMenu( DropdownMenu(
expanded = popupExpanded, expanded = popupExpanded,
@@ -832,49 +834,16 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
} }
if (note.author != accountViewModel.accountLiveData.value?.account?.userProfile()) { if (note.author != accountViewModel.accountLiveData.value?.account?.userProfile()) {
Divider() Divider()
DropdownMenuItem(onClick = { DropdownMenuItem(onClick = { reportDialogShowing = true }) {
note.author?.let { Text("Block / Report")
accountViewModel.hide(it)
}; onDismiss()
}) {
Text(stringResource(R.string.block_hide_user))
}
Divider()
DropdownMenuItem(onClick = {
accountViewModel.report(note, ReportEvent.ReportType.SPAM)
note.author?.let { accountViewModel.hide(it) }
onDismiss()
}) {
Text(stringResource(R.string.report_spam_scam))
}
DropdownMenuItem(onClick = {
accountViewModel.report(note, ReportEvent.ReportType.PROFANITY)
note.author?.let { accountViewModel.hide(it) }
onDismiss()
}) {
Text(stringResource(R.string.report_hateful_speech))
}
DropdownMenuItem(onClick = {
accountViewModel.report(note, ReportEvent.ReportType.IMPERSONATION)
note.author?.let { accountViewModel.hide(it) }
onDismiss()
}) {
Text(stringResource(R.string.report_impersonation))
}
DropdownMenuItem(onClick = {
accountViewModel.report(note, ReportEvent.ReportType.NUDITY)
note.author?.let { accountViewModel.hide(it) }
onDismiss()
}) {
Text(stringResource(R.string.report_nudity_porn))
}
DropdownMenuItem(onClick = {
accountViewModel.report(note, ReportEvent.ReportType.ILLEGAL)
note.author?.let { accountViewModel.hide(it) }
onDismiss()
}) {
Text(stringResource(R.string.report_illegal_behaviour))
} }
} }
} }
if (reportDialogShowing) {
ReportNoteDialog(note = note, accountViewModel = accountViewModel) {
reportDialogShowing = false
onDismiss()
}
}
} }
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -16,6 +17,8 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.AlertDialog import androidx.compose.material.AlertDialog
import androidx.compose.material.Button import androidx.compose.material.Button
import androidx.compose.material.ButtonColors
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Card import androidx.compose.material.Card
import androidx.compose.material.Divider import androidx.compose.material.Divider
import androidx.compose.material.Icon import androidx.compose.material.Icon
@@ -24,11 +27,13 @@ import androidx.compose.material.Text
import androidx.compose.material.TextButton import androidx.compose.material.TextButton
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AlternateEmail import androidx.compose.material.icons.filled.AlternateEmail
import androidx.compose.material.icons.filled.Block
import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.FormatQuote import androidx.compose.material.icons.filled.FormatQuote
import androidx.compose.material.icons.filled.PersonAdd import androidx.compose.material.icons.filled.PersonAdd
import androidx.compose.material.icons.filled.PersonRemove import androidx.compose.material.icons.filled.PersonRemove
import androidx.compose.material.icons.filled.Report
import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Share
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -57,9 +62,11 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.SelectTextDialog import com.vitorpamplona.amethyst.ui.components.SelectTextDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
import com.vitorpamplona.amethyst.ui.theme.WarningColor
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
fun lightenColor(color: Color, amount: Float): Color { private fun lightenColor(color: Color, amount: Float): Color {
var argb = color.toArgb() var argb = color.toArgb()
val hslOut = floatArrayOf(0f, 0f, 0f) val hslOut = floatArrayOf(0f, 0f, 0f)
ColorUtils.colorToHSL(argb, hslOut) ColorUtils.colorToHSL(argb, hslOut)
@@ -71,7 +78,7 @@ fun lightenColor(color: Color, amount: Float): Color {
val externalLinkForNote = { note: Note -> "https://snort.social/e/${note.idNote()}" } val externalLinkForNote = { note: Note -> "https://snort.social/e/${note.idNote()}" }
@Composable @Composable
fun VerticalDivider(color: Color) = private fun VerticalDivider(color: Color) =
Divider( Divider(
color = color, color = color,
modifier = Modifier modifier = Modifier
@@ -88,7 +95,9 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var showSelectTextDialog by remember { mutableStateOf(false) } var showSelectTextDialog by remember { mutableStateOf(false) }
var showDeleteAlertDialog by remember { mutableStateOf(false) } var showDeleteAlertDialog by remember { mutableStateOf(false) }
val isOwnNote = note.author == accountViewModel.userProfile() var showBlockAlertDialog by remember { mutableStateOf(false) }
var showReportDialog by remember { mutableStateOf(false) }
val isOwnNote = note.author == accountViewModel.accountLiveData.value?.account?.userProfile()
val isFollowingUser = !isOwnNote && accountViewModel.isFollowing(note.author!!) val isFollowingUser = !isOwnNote && accountViewModel.isFollowing(note.author!!)
val showToast = { stringResource: Int -> val showToast = { stringResource: Int ->
@@ -134,6 +143,19 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni
showToast(R.string.copied_note_id_to_clipboard) showToast(R.string.copied_note_id_to_clipboard)
onDismiss() onDismiss()
} }
if (!isOwnNote) {
VerticalDivider(primaryLight)
NoteQuickActionItem(Icons.Default.Block, stringResource(R.string.quick_action_block)) {
if (accountViewModel.hideBlockAlertDialog) {
note.author?.let { accountViewModel.hide(it) }
onDismiss()
} else {
showBlockAlertDialog = true
}
}
}
} }
Divider( Divider(
color = primaryLight, color = primaryLight,
@@ -144,7 +166,7 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni
Row(modifier = Modifier.height(IntrinsicSize.Min)) { Row(modifier = Modifier.height(IntrinsicSize.Min)) {
if (isOwnNote) { if (isOwnNote) {
NoteQuickActionItem(Icons.Default.Delete, stringResource(R.string.quick_action_delete)) { NoteQuickActionItem(Icons.Default.Delete, stringResource(R.string.quick_action_delete)) {
if (accountViewModel.hideDeleteRequestInfo()) { if (accountViewModel.hideDeleteRequestDialog) {
accountViewModel.delete(note) accountViewModel.delete(note)
onDismiss() onDismiss()
} else { } else {
@@ -187,6 +209,14 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni
ContextCompat.startActivity(context, shareIntent, null) ContextCompat.startActivity(context, shareIntent, null)
onDismiss() onDismiss()
} }
if (!isOwnNote) {
VerticalDivider(primaryLight)
NoteQuickActionItem(Icons.Default.Report, stringResource(R.string.quick_action_report)) {
showReportDialog = true
}
}
} }
} }
} }
@@ -200,36 +230,24 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni
} }
if (showDeleteAlertDialog) { if (showDeleteAlertDialog) {
AlertDialog( DeleteAlertDialog(note, accountViewModel) {
onDismissRequest = { onDismiss() }, showDeleteAlertDialog = false
title = { onDismiss()
Text(text = stringResource(R.string.quick_action_request_deletion_alert_title)) }
}, }
text = {
Text(text = stringResource(R.string.quick_action_request_deletion_alert_body)) if (showBlockAlertDialog) {
}, BlockAlertDialog(note, accountViewModel) {
buttons = { showBlockAlertDialog = false
Row( onDismiss()
modifier = Modifier.padding(all = 8.dp).fillMaxWidth(), }
horizontalArrangement = Arrangement.SpaceBetween }
) {
TextButton( if (showReportDialog) {
onClick = { ReportNoteDialog(note, accountViewModel) {
accountViewModel.setHideDeleteRequestInfo() showReportDialog = false
accountViewModel.delete(note) onDismiss()
onDismiss() }
}
) {
Text(stringResource(R.string.quick_action_dont_show_again_button))
}
Button(
onClick = { accountViewModel.delete(note); onDismiss() }
) {
Text(stringResource(R.string.quick_action_delete_button))
}
}
}
)
} }
} }
@@ -245,9 +263,99 @@ fun NoteQuickActionItem(icon: ImageVector, label: String, onClick: () -> Unit) {
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(24.dp).padding(bottom = 5.dp), modifier = Modifier
.size(24.dp)
.padding(bottom = 5.dp),
tint = Color.White tint = Color.White
) )
Text(text = label, fontSize = 12.sp, color = Color.White, textAlign = TextAlign.Center) Text(text = label, fontSize = 12.sp, color = Color.White, textAlign = TextAlign.Center)
} }
} }
@Composable
fun DeleteAlertDialog(note: Note, accountViewModel: AccountViewModel, onDismiss: () -> Unit) =
QuickActionAlertDialog(
title = stringResource(R.string.quick_action_request_deletion_alert_title),
textContent = stringResource(R.string.quick_action_request_deletion_alert_body),
buttonIcon = Icons.Default.Delete,
buttonText = stringResource(R.string.quick_action_delete_dialog_btn),
onClickDoOnce = {
accountViewModel.delete(note)
onDismiss()
},
onClickDontShowAgain = {
accountViewModel.delete(note)
accountViewModel.dontShowDeleteRequestDialog()
onDismiss()
},
onDismiss = onDismiss
)
@Composable
private fun BlockAlertDialog(note: Note, accountViewModel: AccountViewModel, onDismiss: () -> Unit) =
QuickActionAlertDialog(
title = stringResource(R.string.report_dialog_block_hide_user_btn),
textContent = stringResource(R.string.report_dialog_blocking_a_user),
buttonIcon = Icons.Default.Block,
buttonText = stringResource(R.string.quick_action_block_dialog_btn),
buttonColors = ButtonDefaults.buttonColors(
backgroundColor = WarningColor,
contentColor = Color.White
),
onClickDoOnce = {
note.author?.let { accountViewModel.hide(it) }
onDismiss()
},
onClickDontShowAgain = {
note.author?.let { accountViewModel.hide(it) }
accountViewModel.dontShowBlockAlertDialog()
onDismiss()
},
onDismiss = onDismiss
)
@Composable
private fun QuickActionAlertDialog(
title: String,
textContent: String,
buttonIcon: ImageVector,
buttonText: String,
buttonColors: ButtonColors = ButtonDefaults.buttonColors(),
onClickDoOnce: () -> Unit,
onClickDontShowAgain: () -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(title)
},
text = {
Text(textContent)
},
buttons = {
Row(
modifier = Modifier
.padding(all = 8.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
TextButton(onClick = onClickDontShowAgain) {
Text(stringResource(R.string.quick_action_dont_show_again_button))
}
Button(onClick = onClickDoOnce, colors = buttonColors) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = buttonIcon,
contentDescription = null
)
Spacer(Modifier.width(8.dp))
Text(buttonText)
}
}
}
}
)
}
@@ -73,8 +73,8 @@ class AccountViewModel(private val account: Account) : ViewModel() {
) )
} }
fun report(note: Note, type: ReportEvent.ReportType) { fun report(note: Note, type: ReportEvent.ReportType, content: String = "") {
account.report(note, type) account.report(note, type, content)
} }
fun report(user: User, type: ReportEvent.ReportType) { fun report(user: User, type: ReportEvent.ReportType) {
@@ -129,11 +129,17 @@ class AccountViewModel(private val account: Account) : ViewModel() {
return account.userProfile().isFollowing(user) return account.userProfile().isFollowing(user)
} }
fun hideDeleteRequestInfo(): Boolean { val hideDeleteRequestDialog: Boolean
return account.hideDeleteRequestInfo get() = account.hideDeleteRequestDialog
fun dontShowDeleteRequestDialog() {
account.setHideDeleteRequestDialog()
} }
fun setHideDeleteRequestInfo() { val hideBlockAlertDialog: Boolean
account.setHideDeleteRequestInfo() get() = account.hideBlockAlertDialog
fun dontShowBlockAlertDialog() {
account.setHideBlockAlertDialog()
} }
} }
@@ -0,0 +1,172 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Block
import androidx.compose.material.icons.filled.Report
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.ui.theme.WarningColor
@Composable
fun ReportNoteDialog(note: Note, accountViewModel: AccountViewModel, onDismiss: () -> Unit) {
val reportTypes = listOf(
Pair(ReportEvent.ReportType.SPAM, stringResource(R.string.report_dialog_spam)),
Pair(ReportEvent.ReportType.PROFANITY, stringResource(R.string.report_dialog_profanity)),
Pair(ReportEvent.ReportType.IMPERSONATION, stringResource(R.string.report_dialog_impersonation)),
Pair(ReportEvent.ReportType.NUDITY, stringResource(R.string.report_dialog_nudity)),
Pair(ReportEvent.ReportType.ILLEGAL, stringResource(R.string.report_dialog_illegal))
)
val reasonOptions = reportTypes.map { it.second }
var additionalReason by remember { mutableStateOf("") }
var selectedReason by remember { mutableStateOf(-1) }
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = "Block and Report") },
navigationIcon = {
IconButton(onClick = onDismiss) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colors.onSurface
)
}
},
backgroundColor = MaterialTheme.colors.surface,
elevation = 0.dp
)
}
) { pad ->
Column(
modifier = Modifier.padding(16.dp, pad.calculateTopPadding(), 16.dp, pad.calculateBottomPadding()),
verticalArrangement = Arrangement.SpaceAround
) {
SpacerH16()
SectionHeader(text = "Block")
SpacerH16()
Text(
text = stringResource(R.string.report_dialog_blocking_a_user)
)
SpacerH16()
ActionButton(
text = stringResource(R.string.report_dialog_block_hide_user_btn),
icon = Icons.Default.Block,
onClick = {
note.author?.let { accountViewModel.hide(it) }
onDismiss()
}
)
SpacerH16()
Divider(color = MaterialTheme.colors.onSurface, thickness = 0.25.dp)
SpacerH16()
SectionHeader(text = stringResource(R.string.report_dialog_report_btn))
SpacerH16()
Text(stringResource(R.string.report_dialog_reminder_public))
SpacerH16()
TextSpinner(
label = stringResource(R.string.report_dialog_select_reason_label),
placeholder = stringResource(R.string.report_dialog_select_reason_placeholder),
options = reasonOptions,
onSelect = {
selectedReason = it
},
modifier = Modifier.fillMaxWidth()
)
SpacerH16()
OutlinedTextField(
value = additionalReason,
onValueChange = { additionalReason = it },
placeholder = { Text(text = stringResource(R.string.report_dialog_additional_reason_placeholder)) },
label = { Text(stringResource(R.string.report_dialog_additional_reason_label)) },
modifier = Modifier.fillMaxWidth()
)
SpacerH16()
ActionButton(
text = stringResource(R.string.report_dialog_post_report_btn),
icon = Icons.Default.Report,
enabled = selectedReason in 0..reportTypes.lastIndex,
onClick = {
accountViewModel.report(note, reportTypes[selectedReason].first, additionalReason)
note.author?.let { accountViewModel.hide(it) }
onDismiss()
}
)
}
}
}
}
@Composable
private fun SpacerH16() = Spacer(modifier = Modifier.height(16.dp))
@Composable
private fun SectionHeader(text: String) = Text(
text = text,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface,
fontSize = 18.sp
)
@Composable
private fun ActionButton(text: String, icon: ImageVector, enabled: Boolean = true, onClick: () -> Unit) = Button(
onClick = onClick,
enabled = enabled,
colors = ButtonDefaults.buttonColors(backgroundColor = WarningColor),
modifier = Modifier.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = Color.White
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = text, color = Color.White)
}
}
@@ -12,3 +12,5 @@ val Following = Color(0xFF03DAC5)
val Nip05 = Color(0xFF01BAFF) val Nip05 = Color(0xFF01BAFF)
val FollowsFollow = Color.Yellow val FollowsFollow = Color.Yellow
val NIP05Verified = Color.Blue val NIP05Verified = Color.Blue
val WarningColor = Color(0xFFC62828)
+35 -17
View File
@@ -197,6 +197,23 @@
<string name="copied_user_id_to_clipboard" tools:ignore="Typos">Copied authors @npub 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="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="select_text_dialog_top">Select Text</string>
<string name="github" translatable="false">Github Gist w/ Proof</string>
<string name="telegram" translatable="false">Telegram</string>
<string name="mastodon" translatable="false">Mastodon Post ID w/ Proof</string>
<string name="twitter" translatable="false">Twitter Status w/ Proof</string>
<string name="github_proof_url_template" translatable="false">https://gist.github.com/&lt;user&gt;/&lt;gist&gt;</string>
<string name="telegram_proof_url_template" translatable="false">https://t.me/&lt;proof post&gt;</string>
<string name="mastodon_proof_url_template" translatable="false">https://&lt;server&gt;/&lt;user&gt;/&lt;proof post&gt;</string>
<string name="twitter_proof_url_template" translatable="false">https://twitter.com/&lt;user&gt;/status/&lt;proof post&gt;</string>
<string name="private_conversation_notification">"&lt;Unable to decrypt private message&gt;\n\nYou were cited in a private/encrypted conversation between %1$s and %2$s."</string>
<string name="account_switch_add_account_dialog_title">Add New Account</string>
<string name="drawer_accounts">Accounts</string>
<string name="account_switch_select_account">Select Account</string>
<string name="account_switch_add_account_btn">Add New Account</string>
<string name="account_switch_active_account">Active account</string>
<string name="account_switch_has_private_key">Has private key</string>
<string name="account_switch_pubkey_only">Read only, no private key</string>
<string name="back">Back</string>
<string name="quick_action_select">Select</string> <string name="quick_action_select">Select</string>
<string name="quick_action_share_browser_link">Share Browser Link</string> <string name="quick_action_share_browser_link">Share Browser Link</string>
<string name="quick_action_share">Share</string> <string name="quick_action_share">Share</string>
@@ -208,24 +225,25 @@
<string name="quick_action_follow">Follow</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_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> <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>
<string name="github" translatable="false">Github Gist w/ Proof</string> <string name="quick_action_block_dialog_btn">Block</string>
<string name="telegram" translatable="false">Telegram</string> <string name="quick_action_delete_dialog_btn">Delete</string>
<string name="mastodon" translatable="false">Mastodon Post ID w/ Proof</string> <string name="quick_action_block">Block</string>
<string name="twitter" translatable="false">Twitter Status w/ Proof</string> <string name="quick_action_report">Report</string>
<string name="github_proof_url_template" translatable="false">https://gist.github.com/&lt;user&gt;/&lt;gist&gt;</string>
<string name="telegram_proof_url_template" translatable="false">https://t.me/&lt;proof post&gt;</string>
<string name="mastodon_proof_url_template" translatable="false">https://&lt;server&gt;/&lt;user&gt;/&lt;proof post&gt;</string>
<string name="twitter_proof_url_template" translatable="false">https://twitter.com/&lt;user&gt;/status/&lt;proof post&gt;</string>
<string name="private_conversation_notification">"&lt;Unable to decrypt private message&gt;\n\nYou were cited in a private/encrypted conversation between %1$s and %2$s."</string>
<string name="quick_action_delete_button">Delete</string> <string name="quick_action_delete_button">Delete</string>
<string name="quick_action_dont_show_again_button">Don\'t show again</string> <string name="quick_action_dont_show_again_button">Don\'t show again</string>
<string name="account_switch_add_account_dialog_title">Add New Account</string> <string name="report_dialog_spam">Spam or scams</string>
<string name="drawer_accounts">Accounts</string> <string name="report_dialog_profanity">Profanity or hateful conduct</string>
<string name="account_switch_select_account">Select Account</string> <string name="report_dialog_impersonation">Malicious impersonation</string>
<string name="account_switch_add_account_btn">Add New Account</string> <string name="report_dialog_nudity">Nudity or graphic content</string>
<string name="account_switch_active_account">Active account</string> <string name="report_dialog_illegal">Illegal Behavior</string>
<string name="account_switch_has_private_key">Has private key</string> <string name="report_dialog_blocking_a_user">Blocking a user will hide their content in your app. Your notes are still publicly viewable, including to people you block. Blocked users are listed on the Security Filters screen.</string>
<string name="account_switch_pubkey_only">Read only, no private key</string> <string name="report_dialog_block_hide_user_btn"><![CDATA[Block & Hide User]]></string>
<string name="back">Back</string> <string name="report_dialog_report_btn">Report Abuse</string>
<string name="report_dialog_reminder_public">All reports posted will be publicly visible.</string>
<string name="report_dialog_additional_reason_placeholder">Optionally provide additional context about your report…</string>
<string name="report_dialog_additional_reason_label">Additional Context</string>
<string name="report_dialog_select_reason_label">Reason</string>
<string name="report_dialog_select_reason_placeholder">Select a reason…</string>
<string name="report_dialog_post_report_btn">Post Report</string>
</resources> </resources>