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 1fe879bea..b6ada4a6c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -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) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt index 1920085e0..63b51c21b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt @@ -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()) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt new file mode 100644 index 000000000..acd75cd5b --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt @@ -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, 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, 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) + } + } + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 71edb6a09..ee19c865a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -344,7 +344,7 @@ fun ChatroomMessageCompose( } } - NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) + NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) } } } 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 ac10c138e..8c228a767 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 @@ -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() + } + } } 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 index 4f70ca414..912f579bb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -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)) + } + } + } + ) +} 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 bd84aa779..2b40c8399 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 @@ -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) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt new file mode 100644 index 000000000..e3a7571e6 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt @@ -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) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9874b9513..877d9122d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -228,5 +228,19 @@ Has private key Read only, no private key Back + Spam or scams + Profanity or hateful conduct + Malicious impersonation + Nudity or graphic content + Illegal Behavior + Blocking a user will hide their content in your app. Your notes are still publicly viewable, including to people you block. + + Report Abuse + All reports posted will be publicly visible. + Optionally provide additional context about your report... + Additional Context + Reason + Select a reason... + Post Report