diff --git a/.gitignore b/.gitignore index cf483fc1e..d67bb1a41 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,7 @@ render.experimental.xml .idea/**/caches/ .idea/**/libraries/ .idea/**/shelf/ +.idea/**/codeStyles .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/.name diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 7adb1244d..9b96d1430 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -47,7 +47,8 @@ private object PrefKeys { const val TRANSLATE_TO = "translateTo" const val ZAP_AMOUNTS = "zapAmounts" 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" } } @@ -183,7 +184,8 @@ object LocalPreferences { putString(PrefKeys.TRANSLATE_TO, account.translateTo) putString(PrefKeys.ZAP_AMOUNTS, gson.toJson(account.zapAmountChoices)) 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.PROFILE_PICTURE_URL, account.userProfile().profilePicture()) }.apply() @@ -230,7 +232,8 @@ object LocalPreferences { 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( Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()), @@ -241,7 +244,8 @@ object LocalPreferences { languagePreferences, translateTo, zapAmountChoices, - hideDeleteRequestInfo, + hideDeleteRequestDialog, + hideBlockAlertDialog, latestContactList ) } @@ -289,8 +293,8 @@ object LocalPreferences { stringPrefs.forEach { userPrefs.putString(it, appPrefs.getString(it, null)) } stringSetPrefs.forEach { userPrefs.putStringSet(it, appPrefs.getStringSet(it, null)) } userPrefs.putBoolean( - PrefKeys.HIDE_DELETE_REQUEST_INFO, - appPrefs.getBoolean(PrefKeys.HIDE_DELETE_REQUEST_INFO, false) + PrefKeys.HIDE_DELETE_REQUEST_DIALOG, + appPrefs.getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false) ) }.apply() } 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..484edafa2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -59,7 +59,8 @@ class Account( var languagePreferences: Map = mapOf(), var translateTo: String = Locale.getDefault().language, var zapAmountChoices: List = listOf(500L, 1000L, 5000L), - var hideDeleteRequestInfo: Boolean = false, + var hideDeleteRequestDialog: Boolean = false, + var hideBlockAlertDialog: Boolean = false, var backupContactList: ContactListEvent? = null ) { var transientHiddenUsers: Set = setOf() @@ -170,7 +171,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 +186,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) } @@ -553,8 +554,13 @@ class Account( saveable.invalidateData() } - fun setHideDeleteRequestInfo() { - hideDeleteRequestInfo = true + fun setHideDeleteRequestDialog() { + hideDeleteRequestDialog = true + saveable.invalidateData() + } + + fun setHideBlockAlertDialog() { + hideBlockAlertDialog = true saveable.invalidateData() } 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..170ba719c 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 @@ -7,6 +7,7 @@ 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.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth 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.material.AlertDialog import androidx.compose.material.Button +import androidx.compose.material.ButtonColors +import androidx.compose.material.ButtonDefaults import androidx.compose.material.Card import androidx.compose.material.Divider import androidx.compose.material.Icon @@ -24,11 +27,13 @@ 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 import androidx.compose.material.icons.filled.PersonAdd import androidx.compose.material.icons.filled.PersonRemove +import androidx.compose.material.icons.filled.Report import androidx.compose.material.icons.filled.Share import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -57,9 +62,11 @@ 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 com.vitorpamplona.amethyst.ui.theme.WarningColor import kotlinx.coroutines.launch -fun lightenColor(color: Color, amount: Float): Color { +private fun lightenColor(color: Color, amount: Float): Color { var argb = color.toArgb() val hslOut = floatArrayOf(0f, 0f, 0f) 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()}" } @Composable -fun VerticalDivider(color: Color) = +private fun VerticalDivider(color: Color) = Divider( color = color, modifier = Modifier @@ -88,7 +95,9 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni val scope = rememberCoroutineScope() var showSelectTextDialog 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 showToast = { stringResource: Int -> @@ -134,6 +143,19 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni showToast(R.string.copied_note_id_to_clipboard) 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( color = primaryLight, @@ -144,7 +166,7 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni Row(modifier = Modifier.height(IntrinsicSize.Min)) { if (isOwnNote) { NoteQuickActionItem(Icons.Default.Delete, stringResource(R.string.quick_action_delete)) { - if (accountViewModel.hideDeleteRequestInfo()) { + if (accountViewModel.hideDeleteRequestDialog) { accountViewModel.delete(note) onDismiss() } else { @@ -187,6 +209,14 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni ContextCompat.startActivity(context, shareIntent, null) 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) { - 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) { + showDeleteAlertDialog = false + onDismiss() + } + } + + if (showBlockAlertDialog) { + BlockAlertDialog(note, accountViewModel) { + showBlockAlertDialog = false + onDismiss() + } + } + + if (showReportDialog) { + ReportNoteDialog(note, accountViewModel) { + showReportDialog = false + onDismiss() + } } } @@ -245,9 +263,99 @@ 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) = + 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) + } + } + } + } + ) +} 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..3f94694d7 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) { @@ -129,11 +129,17 @@ class AccountViewModel(private val account: Account) : ViewModel() { return account.userProfile().isFollowing(user) } - fun hideDeleteRequestInfo(): Boolean { - return account.hideDeleteRequestInfo + val hideDeleteRequestDialog: Boolean + get() = account.hideDeleteRequestDialog + + fun dontShowDeleteRequestDialog() { + account.setHideDeleteRequestDialog() } - fun setHideDeleteRequestInfo() { - account.setHideDeleteRequestInfo() + val hideBlockAlertDialog: Boolean + get() = account.hideBlockAlertDialog + + fun dontShowBlockAlertDialog() { + account.setHideBlockAlertDialog() } } 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..db2fc1b3d --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt @@ -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) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt index 0186afcd4..f2609a161 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt @@ -12,3 +12,5 @@ val Following = Color(0xFF03DAC5) val Nip05 = Color(0xFF01BAFF) val FollowsFollow = Color.Yellow val NIP05Verified = Color.Blue + +val WarningColor = Color(0xFFC62828) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dfddc4d1b..e306ac3bf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -197,6 +197,23 @@ Copied author’s @npub to clipboard Copied note ID (@note1) to clipboard Select Text + Github Gist w/ Proof + Telegram + Mastodon Post ID w/ Proof + Twitter Status w/ Proof + https://gist.github.com/<user>/<gist> + https://t.me/<proof post> + https://<server>/<user>/<proof post> + https://twitter.com/<user>/status/<proof post> + "<Unable to decrypt private message>\n\nYou were cited in a private/encrypted conversation between %1$s and %2$s." + Add New Account + Accounts + Select Account + Add New Account + Active account + Has private key + Read only, no private key + Back Select Share Browser Link Share @@ -208,24 +225,25 @@ Follow Request Deletion 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. - Github Gist w/ Proof - Telegram - Mastodon Post ID w/ Proof - Twitter Status w/ Proof - https://gist.github.com/<user>/<gist> - https://t.me/<proof post> - https://<server>/<user>/<proof post> - https://twitter.com/<user>/status/<proof post> - "<Unable to decrypt private message>\n\nYou were cited in a private/encrypted conversation between %1$s and %2$s." + Block + Delete + Block + Report Delete Don\'t show again - Add New Account - Accounts - Select Account - Add New Account - Active account - 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. Blocked users are listed on the Security Filters screen. + + Report Abuse + All reports posted will be publicly visible. + Optionally provide additional context about your report… + Additional Context + Reason + Select a reason… + Post Report