feat: make report dialogues up to date, add user report

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2023-11-11 17:48:58 +01:00
parent f611296670
commit 9da6b040a8
6 changed files with 432 additions and 50 deletions

View File

@ -12,8 +12,16 @@ import kotlinx.serialization.encoding.Encoder
enum class ContentReportReason(val value: String) {
NoneSpecified("NoneSpecified"),
Illegal("Illegal"),
IllegalGoods("IllegalGoods"),
IllegalExtortion("IllegalExtortion"),
IllegalPornography("IllegalPornography"),
IllegalHacking("IllegalHacking"),
ExtremeViolence("ExtremeViolence"),
PromotesHarm("PromotesHarm"),
UnsolicitedSpam("UnsolicitedSpam"),
Raid("Raid"),
SpamAbuse("SpamAbuse"),
ScamsFraud("ScamsFraud"),
Malware("Malware"),
Harassment("Harassment");
@ -30,8 +38,16 @@ enum class ContentReportReason(val value: String) {
when (val value = decoder.decodeString()) {
"NoneSpecified" -> NoneSpecified
"Illegal" -> Illegal
"IllegalGoods" -> IllegalGoods
"IllegalExtortion" -> IllegalExtortion
"IllegalPornography" -> IllegalPornography
"IllegalHacking" -> IllegalHacking
"ExtremeViolence" -> ExtremeViolence
"PromotesHarm" -> PromotesHarm
"UnsolicitedSpam" -> UnsolicitedSpam
"Raid" -> Raid
"SpamAbuse" -> SpamAbuse
"ScamsFraud" -> ScamsFraud
"Malware" -> Malware
"Harassment" -> Harassment
else -> throw IllegalArgumentException("Unknown ContentReportReason: $value")
@ -46,6 +62,7 @@ enum class ContentReportReason(val value: String) {
@Serializable
enum class UserReportReason(val value: String) {
NoneSpecified("NoneSpecified"),
UnsolicitedSpam("UnsolicitedSpam"),
SpamAbuse("SpamAbuse"),
InappropriateProfile("InappropriateProfile"),
Impersonation("Impersonation"),
@ -64,6 +81,7 @@ enum class UserReportReason(val value: String) {
override fun deserialize(decoder: Decoder): UserReportReason =
when (val value = decoder.decodeString()) {
"NoneSpecified" -> NoneSpecified
"UnsolicitedSpam" -> UnsolicitedSpam
"SpamAbuse" -> SpamAbuse
"InappropriateProfile" -> InappropriateProfile
"Impersonation" -> Impersonation

View File

@ -54,7 +54,7 @@ fun SelfUserOverview() {
}
@Composable
fun UserOverview(user: User) {
fun UserOverview(user: User, internalPadding: Boolean = true) {
var profile by remember { mutableStateOf<Profile?>(null) }
LaunchedEffect(user) {
@ -67,7 +67,7 @@ fun UserOverview(user: User) {
}
}
RawUserOverview(user, profile)
RawUserOverview(user, profile, internalPadding = internalPadding)
}
@Composable
@ -75,7 +75,8 @@ fun RawUserOverview(
user: User,
profile: Profile? = null,
pfpUrl: String? = null,
backgroundUrl: String? = null
backgroundUrl: String? = null,
internalPadding: Boolean = true
) {
val context = LocalContext.current
var teamMemberFlair by remember { mutableStateOf<Brush?>(null) }
@ -94,7 +95,7 @@ fun RawUserOverview(
Box(
contentAlignment = Alignment.BottomStart,
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(horizontal = if (internalPadding) 16.dp else 0.dp)
.clip(MaterialTheme.shapes.large)
.then(
if (user.id in SpecialUsers.TEAM_MEMBER_FLAIRS.keys) {

View File

@ -23,7 +23,6 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.DismissibleDrawerSheet
import androidx.compose.material3.DismissibleNavigationDrawer
import androidx.compose.material3.DrawerState
@ -95,6 +94,7 @@ import chat.revolt.internals.Changelogs
import chat.revolt.ndk.Pipebomb
import chat.revolt.persistence.KVStorage
import chat.revolt.screens.chat.dialogs.safety.ReportMessageDialog
import chat.revolt.screens.chat.dialogs.safety.ReportUserDialog
import chat.revolt.screens.chat.views.FriendsScreen
import chat.revolt.screens.chat.views.HomeScreen
import chat.revolt.screens.chat.views.NoCurrentChannelScreen
@ -1050,16 +1050,10 @@ fun ChannelNavigator(
dialog("report/user/{userId}") { backStackEntry ->
val userId = backStackEntry.arguments?.getString("userId")
if (userId != null) {
AlertDialog(onDismissRequest = {
navController.popBackStack()
}) {
Text("Report user $userId")
Button(onClick = {
navController.popBackStack()
}) {
Text("Close")
}
}
ReportUserDialog(
navController = navController,
userId = userId
)
}
}
}

View File

@ -19,6 +19,9 @@ import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconToggleButton
import androidx.compose.material3.MaterialTheme
@ -50,13 +53,14 @@ import chat.revolt.components.chat.Message
import chat.revolt.components.generic.FormTextField
import kotlinx.coroutines.launch
enum class ReportFlowState {
enum class MessageReportFlowState {
Reason,
Sending,
Done,
Error
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReportMessageDialog(navController: NavController, messageId: String) {
val message = RevoltAPI.messageCache[messageId]
@ -68,20 +72,28 @@ fun ReportMessageDialog(navController: NavController, messageId: String) {
val author = RevoltAPI.userCache[message.author]
val messageIsBridged = author?.let { author.bot != null && message.masquerade != null } ?: false
val state = remember { mutableStateOf(ReportFlowState.Reason) }
val state = remember { mutableStateOf(MessageReportFlowState.Reason) }
val selectedReason = remember { mutableStateOf("Illegal") }
val userAddedContext = remember { mutableStateOf("") }
when (state.value) {
ReportFlowState.Reason -> {
MessageReportFlowState.Reason -> {
val reasons = mapOf(
"Illegal" to stringResource(id = R.string.report_reason_content_illegal),
"IllegalGoods" to stringResource(id = R.string.report_reason_content_illegal_goods),
"IllegalExtortion" to stringResource(id = R.string.report_reason_content_illegal_extortion),
"IllegalPornography" to stringResource(id = R.string.report_reason_content_illegal_pornography),
"IllegalHacking" to stringResource(id = R.string.report_reason_content_illegal_hacking),
"ExtremeViolence" to stringResource(id = R.string.report_reason_content_extreme_violence),
"PromotesHarm" to stringResource(id = R.string.report_reason_content_promotes_harm),
"UnsolicitedSpam" to stringResource(id = R.string.report_reason_content_unsolicited_spam),
"Raid" to stringResource(id = R.string.report_reason_content_raid),
"SpamAbuse" to stringResource(id = R.string.report_reason_content_spam_abuse),
"ScamsFraud" to stringResource(id = R.string.report_reason_content_scams_fraud),
"Malware" to stringResource(id = R.string.report_reason_content_malware),
"Harassment" to stringResource(id = R.string.report_reason_content_harassment),
"Other" to stringResource(id = R.string.report_reason_content_other)
"NoneSpecified" to stringResource(id = R.string.report_reason_content_other)
)
val reasonDropdownExpanded = remember { mutableStateOf(false) }
@ -91,7 +103,7 @@ fun ReportMessageDialog(navController: NavController, messageId: String) {
},
title = {
Text(
text = stringResource(id = R.string.report),
text = stringResource(id = R.string.report_message_heading),
modifier = Modifier.fillMaxWidth()
)
},
@ -134,38 +146,30 @@ fun ReportMessageDialog(navController: NavController, messageId: String) {
Spacer(modifier = Modifier.height(16.dp))
Box {
ExposedDropdownMenuBox(
expanded = reasonDropdownExpanded.value,
onExpandedChange = {
reasonDropdownExpanded.value = it
},
) {
TextField(
value = reasons[selectedReason.value]
?: stringResource(id = R.string.unknown),
onValueChange = {
selectedReason.value = it
},
readOnly = true,
onValueChange = {},
label = {
Text(
text = stringResource(id = R.string.report_reason)
)
},
readOnly = true,
trailingIcon = {
IconToggleButton(
checked = reasonDropdownExpanded.value,
onCheckedChange = {
reasonDropdownExpanded.value = it
}
) {
Icon(
imageVector = Icons.Default.ArrowDropDown,
contentDescription = stringResource(
id = R.string.report_reason
)
)
}
ExposedDropdownMenuDefaults.TrailingIcon(expanded = reasonDropdownExpanded.value)
},
modifier = Modifier.fillMaxWidth()
colors = ExposedDropdownMenuDefaults.textFieldColors(),
modifier = Modifier.menuAnchor()
)
DropdownMenu(
ExposedDropdownMenu(
expanded = reasonDropdownExpanded.value,
onDismissRequest = {
reasonDropdownExpanded.value = false
@ -216,7 +220,7 @@ fun ReportMessageDialog(navController: NavController, messageId: String) {
confirmButton = {
TextButton(
onClick = {
state.value = ReportFlowState.Sending
state.value = MessageReportFlowState.Sending
},
modifier = Modifier.testTag("report_send")
) {
@ -226,7 +230,7 @@ fun ReportMessageDialog(navController: NavController, messageId: String) {
)
}
ReportFlowState.Sending -> {
MessageReportFlowState.Sending -> {
AlertDialog(
onDismissRequest = {},
title = {
@ -250,9 +254,9 @@ fun ReportMessageDialog(navController: NavController, messageId: String) {
ContentReportReason.valueOf(selectedReason.value),
userAddedContext.value
)
state.value = ReportFlowState.Done
state.value = MessageReportFlowState.Done
} catch (e: Error) {
state.value = ReportFlowState.Error
state.value = MessageReportFlowState.Error
Log.e("ReportMessageDialog", "Failed to report message", e)
return@launch
}
@ -265,7 +269,7 @@ fun ReportMessageDialog(navController: NavController, messageId: String) {
)
}
ReportFlowState.Done -> {
MessageReportFlowState.Done -> {
val scope = rememberCoroutineScope()
AlertDialog(
@ -334,7 +338,7 @@ fun ReportMessageDialog(navController: NavController, messageId: String) {
)
}
ReportFlowState.Error -> {
MessageReportFlowState.Error -> {
AlertDialog(
onDismissRequest = {
navController.popBackStack()

View File

@ -0,0 +1,353 @@
package chat.revolt.screens.chat.dialogs.safety
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconToggleButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import chat.revolt.R
import chat.revolt.api.RevoltAPI
import chat.revolt.api.routes.safety.putMessageReport
import chat.revolt.api.routes.safety.putUserReport
import chat.revolt.api.routes.user.blockUser
import chat.revolt.api.schemas.ContentReportReason
import chat.revolt.api.schemas.UserReportReason
import chat.revolt.components.chat.Message
import chat.revolt.components.generic.FormTextField
import chat.revolt.components.screens.settings.UserOverview
import kotlinx.coroutines.launch
enum class UserReportFlowState {
Reason,
Sending,
Done,
Error
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReportUserDialog(navController: NavController, userId: String) {
val user = RevoltAPI.userCache[userId]
if (user == null) {
navController.popBackStack()
return
}
val state = remember { mutableStateOf(UserReportFlowState.Reason) }
val selectedReason = remember { mutableStateOf("UnsolicitedSpam") }
val userAddedContext = remember { mutableStateOf("") }
when (state.value) {
UserReportFlowState.Reason -> {
val reasons = mapOf(
"UnsolicitedSpam" to stringResource(id = R.string.report_reason_user_unsolicited_spam),
"SpamAbuse" to stringResource(id = R.string.report_reason_user_spam_abuse),
"InappropriateProfile" to stringResource(id = R.string.report_reason_user_inappropriate_content),
"Impersonation" to stringResource(id = R.string.report_reason_user_impersonation),
"BanEvasion" to stringResource(id = R.string.report_reason_user_ban_evasion),
"Underage" to stringResource(id = R.string.report_reason_user_underage),
"NoneSpecified" to stringResource(id = R.string.report_reason_user_other)
)
val reasonDropdownExpanded = remember { mutableStateOf(false) }
AlertDialog(
onDismissRequest = {
// nothing - prevent mistaps from closing the dialog
},
title = {
Text(
text = stringResource(id = R.string.report_user_heading),
modifier = Modifier.fillMaxWidth()
)
},
text = {
Column {
Text(text = stringResource(id = R.string.report_user))
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(id = R.string.report_user_preview),
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp))
UserOverview(user, internalPadding = false)
Spacer(modifier = Modifier.height(16.dp))
ExposedDropdownMenuBox(
expanded = reasonDropdownExpanded.value,
onExpandedChange = {
reasonDropdownExpanded.value = it
},
) {
TextField(
value = reasons[selectedReason.value]
?: stringResource(id = R.string.unknown),
readOnly = true,
onValueChange = {},
label = {
Text(
text = stringResource(id = R.string.report_reason)
)
},
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = reasonDropdownExpanded.value)
},
colors = ExposedDropdownMenuDefaults.textFieldColors(),
modifier = Modifier.menuAnchor()
)
ExposedDropdownMenu(
expanded = reasonDropdownExpanded.value,
onDismissRequest = {
reasonDropdownExpanded.value = false
}
) {
reasons.forEach { (key, value) ->
DropdownMenuItem(
text = {
Text(text = value)
},
onClick = {
selectedReason.value = key
reasonDropdownExpanded.value = false
}
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
FormTextField(
value = userAddedContext.value,
label = stringResource(id = R.string.report_reason_additional),
onChange = {
userAddedContext.value = it
},
supportingText = {
Text(
text = stringResource(
id = R.string.report_reason_additional_hint
)
)
}
)
}
},
dismissButton = {
TextButton(
onClick = {
navController.popBackStack()
},
modifier = Modifier.testTag("report_cancel")
) {
Text(text = stringResource(id = R.string.report_cancel))
}
},
confirmButton = {
TextButton(
onClick = {
state.value = UserReportFlowState.Sending
},
modifier = Modifier.testTag("report_send")
) {
Text(text = stringResource(id = R.string.report_submit))
}
}
)
}
UserReportFlowState.Sending -> {
AlertDialog(
onDismissRequest = {},
title = {
Text(
text = stringResource(id = R.string.report_submitting),
modifier = Modifier.fillMaxWidth()
)
},
text = {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator()
LaunchedEffect(true) {
launch {
try {
Log.d("ReportMessageDialog", "Reporting user $userId")
putUserReport(
userId,
UserReportReason.valueOf(selectedReason.value),
userAddedContext.value
)
state.value = UserReportFlowState.Done
} catch (e: Error) {
state.value = UserReportFlowState.Error
Log.e("ReportMessageDialog", "Failed to report user", e)
return@launch
}
}
}
}
},
dismissButton = {},
confirmButton = {}
)
}
UserReportFlowState.Done -> {
val scope = rememberCoroutineScope()
AlertDialog(
onDismissRequest = {
navController.popBackStack()
},
icon = {
Icon(
imageVector = Icons.Default.Check,
contentDescription = null, // decorative
tint = MaterialTheme.colorScheme.primary
)
},
title = {
Text(
text = stringResource(id = R.string.report_submit_success),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
},
text = {
Column {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(id = R.string.report_submit_thanks),
textAlign = TextAlign.Center
)
}
Spacer(modifier = Modifier.height(32.dp))
Text(
text = stringResource(id = R.string.report_block_question),
textAlign = TextAlign.Center,
fontSize = 16.sp,
fontWeight = FontWeight.Bold
)
}
},
dismissButton = {
TextButton(
onClick = {
navController.popBackStack()
},
modifier = Modifier.testTag("report_block_no")
) {
Text(text = stringResource(id = R.string.report_block_no))
}
},
confirmButton = {
TextButton(
onClick = {
scope.launch {
blockUser(userId)
}
navController.popBackStack()
},
modifier = Modifier.testTag("report_block_yes")
) {
Text(text = stringResource(id = R.string.report_block_yes))
}
}
)
}
UserReportFlowState.Error -> {
AlertDialog(
onDismissRequest = {
navController.popBackStack()
},
icon = {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null, // decorative
tint = MaterialTheme.colorScheme.primary
)
},
title = {
Text(
text = stringResource(id = R.string.report_submit_error_header),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
},
text = {
Column {
Text(
text = stringResource(id = R.string.report_submit_error),
textAlign = TextAlign.Center
)
}
},
dismissButton = {
TextButton(
onClick = {
navController.popBackStack()
},
modifier = Modifier.testTag("report_error_ok")
) {
Text(text = stringResource(id = R.string.ok))
}
},
confirmButton = {}
)
}
}
}

View File

@ -303,23 +303,35 @@
<string name="report">Report</string>
<string name="report_cancel">Cancel</string>
<string name="report_message">Thank you for taking the time to report this message. Please provide a reason for reporting this message.</string>
<string name="report_message_heading">Tell us what\'s wrong with this message</string>
<string name="report_message">Thank you for taking the time to report this message. Your report will not be shared with the user who sent the message or the server\'s moderators.</string>
<string name="report_message_preview">Selected message:</string>
<string name="report_message_bridge_notice">Note: This message may have been sent from another platform. It is recommended to also report the message on the platform it was sent from.</string>
<string name="report_server">Thank you for taking the time to report this server. Please provide a reason for reporting this server.</string>
<string name="report_server_heading">Tell us what\'s wrong with this server</string>
<string name="report_server">Thank you for taking the time to report this server. Your report will not be shared with the server\'s moderators.</string>
<string name="report_server_preview">Selected server:</string>
<string name="report_user">Thank you for taking the time to report this user. Please provide a reason for reporting this user.</string>
<string name="report_user_heading">Tell us what\'s wrong with this user</string>
<string name="report_user">Thank you for taking the time to report this user. Your report will not be shared with the user or the server\'s moderators.</string>
<string name="report_user_preview">Selected user:</string>
<string name="report_reason">Reason</string>
<string name="report_reason">Pick a category</string>
<string name="report_reason_content_illegal">Illegal content</string>
<string name="report_reason_content_illegal_goods">Drugs or other illegal goods</string>
<string name="report_reason_content_illegal_extortion">Extortion or blackmail</string>
<string name="report_reason_content_illegal_pornography">Revenge or underage pornography</string>
<string name="report_reason_content_illegal_hacking">Illegal hacking or cracking</string>
<string name="report_reason_content_extreme_violence">Extreme violence, gore or animal cruelty</string>
<string name="report_reason_content_promotes_harm">Promotes harm</string>
<string name="report_reason_content_unsolicited_spam">Unsolicited advertising or spam</string>
<string name="report_reason_content_raid">Raid or spam attack</string>
<string name="report_reason_content_spam_abuse">Spam or similar platform abuse</string>
<string name="report_reason_content_scams_fraud">Scams or fraud</string>
<string name="report_reason_content_malware">Malware or phishing</string>
<string name="report_reason_content_harassment">Harassment or cyberbullying</string>
<string name="report_reason_content_other">Other</string>
<string name="report_reason_user_unsolicited_spam">Unsolicited advertising or spam</string>
<string name="report_reason_user_spam_abuse">Spam or similar platform abuse</string>
<string name="report_reason_user_inappropriate_content">Inappropriate content (like NSFW)</string>
<string name="report_reason_user_impersonation">Impersonation</string>