Add report & block screen

pull/275/head
maxmoney21m 2023-03-15 01:41:39 +08:00
rodzic 0272073636
commit 0241bf0913
9 zmienionych plików z 370 dodań i 81 usunięć

Wyświetl plik

@ -170,7 +170,7 @@ class Account(
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 (note.hasReacted(userProfile(), "⚠️")) {
@ -185,7 +185,7 @@ class Account(
}
note.event?.let {
val event = ReportEvent.create(it, type, loggedIn.privKey!!)
val event = ReportEvent.create(it, type, loggedIn.privKey!!, content = content)
Client.send(event)
LocalCache.consume(event, null)
}

Wyświetl plik

@ -57,9 +57,13 @@ class ReportEvent(
companion object {
const val kind = 1984
fun create(reportedPost: EventInterface, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent {
val content = ""
fun create(
reportedPost: EventInterface,
type: ReportType,
privateKey: ByteArray,
content: String = "",
createdAt: Long = Date().time / 1000
): ReportEvent {
val reportPostTag = listOf("e", reportedPost.id(), type.name.lowercase())
val reportAuthorTag = listOf("p", reportedPost.pubKey(), type.name.lowercase())

Wyświetl plik

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

Wyświetl plik

@ -344,7 +344,7 @@ fun ChatroomMessageCompose(
}
}
NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
}
}
}

Wyświetl plik

@ -59,6 +59,7 @@ import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
import com.vitorpamplona.amethyst.ui.theme.Following
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -779,6 +780,7 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
val clipboardManager = LocalClipboardManager.current
val appContext = LocalContext.current.applicationContext
val actContext = LocalContext.current
var reportDialogShowing by remember { mutableStateOf(false) }
DropdownMenu(
expanded = popupExpanded,
@ -832,49 +834,16 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
}
if (note.author != accountViewModel.accountLiveData.value?.account?.userProfile()) {
Divider()
DropdownMenuItem(onClick = {
note.author?.let {
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))
DropdownMenuItem(onClick = { reportDialogShowing = true }) {
Text("Block / Report")
}
}
}
if (reportDialogShowing) {
ReportNoteDialog(note = note, accountViewModel = accountViewModel) {
reportDialogShowing = false
onDismiss()
}
}
}

Wyświetl plik

@ -24,6 +24,7 @@ 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.Block
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.FormatQuote
@ -57,6 +58,7 @@ 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 com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
import kotlinx.coroutines.launch
fun lightenColor(color: Color, amount: Float): Color {
@ -88,6 +90,7 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni
val scope = rememberCoroutineScope()
var showSelectTextDialog by remember { mutableStateOf(false) }
var showDeleteAlertDialog by remember { mutableStateOf(false) }
var showReportDialog by remember { mutableStateOf(false) }
val isOwnNote = note.author == accountViewModel.userProfile()
val isFollowingUser = !isOwnNote && accountViewModel.isFollowing(note.author!!)
@ -134,6 +137,14 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni
showToast(R.string.copied_note_id_to_clipboard)
onDismiss()
}
if (note.author != accountViewModel.accountLiveData.value?.account?.userProfile()) {
VerticalDivider(primaryLight)
NoteQuickActionItem(Icons.Default.Block, "Block") {
showReportDialog = true
}
}
}
Divider(
color = primaryLight,
@ -187,6 +198,7 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni
ContextCompat.startActivity(context, shareIntent, null)
onDismiss()
}
VerticalDivider(primaryLight)
}
}
}
@ -200,36 +212,14 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni
}
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(stringResource(R.string.quick_action_dont_show_again_button))
}
Button(
onClick = { accountViewModel.delete(note); onDismiss() }
) {
Text(stringResource(R.string.quick_action_delete_button))
}
}
}
)
DeleteAlertDialog(note, accountViewModel, onDismiss)
}
if (showReportDialog) {
ReportNoteDialog(note, accountViewModel) {
showReportDialog = false
onDismiss()
}
}
}
@ -245,9 +235,47 @@ fun NoteQuickActionItem(icon: ImageVector, label: String, onClick: () -> Unit) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(24.dp).padding(bottom = 5.dp),
modifier = Modifier
.size(24.dp)
.padding(bottom = 5.dp),
tint = Color.White
)
Text(text = label, fontSize = 12.sp, color = Color.White, textAlign = TextAlign.Center)
}
}
@Composable
fun DeleteAlertDialog(note: Note, accountViewModel: AccountViewModel, onDismiss: () -> Unit) {
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(stringResource(R.string.quick_action_dont_show_again_button))
}
Button(
onClick = { accountViewModel.delete(note); onDismiss() }
) {
Text(stringResource(R.string.quick_action_delete_button))
}
}
}
)
}

Wyświetl plik

@ -73,8 +73,8 @@ class AccountViewModel(private val account: Account) : ViewModel() {
)
}
fun report(note: Note, type: ReportEvent.ReportType) {
account.report(note, type)
fun report(note: Note, type: ReportEvent.ReportType, content: String = "") {
account.report(note, type, content)
}
fun report(user: User, type: ReportEvent.ReportType) {

Wyświetl plik

@ -0,0 +1,173 @@
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
@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()
}
)
}
}
}
}
private val warningColor = Color(0xFFC62828)
@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)
}
}

Wyświetl plik

@ -228,5 +228,19 @@
<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="report_dialog_spam">Spam or scams</string>
<string name="report_dialog_profanity">Profanity or hateful conduct</string>
<string name="report_dialog_impersonation">Malicious impersonation</string>
<string name="report_dialog_nudity">Nudity or graphic content</string>
<string name="report_dialog_illegal">Illegal Behavior</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.</string>
<string name="report_dialog_block_hide_user_btn"><![CDATA[Block & Hide User]]></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>