diff --git a/app/src/main/java/chat/revolt/components/screens/settings/ServerOverview.kt b/app/src/main/java/chat/revolt/components/screens/settings/ServerOverview.kt
new file mode 100644
index 00000000..78b0eaf3
--- /dev/null
+++ b/app/src/main/java/chat/revolt/components/screens/settings/ServerOverview.kt
@@ -0,0 +1,131 @@
+package chat.revolt.components.screens.settings
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+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.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import chat.revolt.R
+import chat.revolt.api.REVOLT_FILES
+import chat.revolt.api.schemas.Server
+import chat.revolt.api.schemas.ServerFlags
+import chat.revolt.api.schemas.has
+import chat.revolt.components.generic.IconPlaceholder
+import chat.revolt.components.generic.RemoteImage
+
+@Composable
+fun ServerOverview(server: Server) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(MaterialTheme.shapes.large),
+ contentAlignment = Alignment.BottomStart
+ ) {
+ server.banner?.let {
+ Box(
+ modifier = Modifier
+ .background(Color.Black.copy(alpha = 0.25f))
+ .height(166.dp)
+ .fillMaxWidth()
+ )
+
+ RemoteImage(
+ url = "$REVOLT_FILES/banners/${it.id}",
+ description = null,
+ modifier = Modifier
+ .height(166.dp)
+ .fillMaxWidth(),
+ contentScale = ContentScale.FillWidth
+ )
+
+ Box(
+ modifier = Modifier
+ .background(
+ Brush.verticalGradient(
+ listOf(
+ Color.Transparent,
+ Color.Black.copy(alpha = 0.7f)
+ )
+ )
+ )
+ .height(166.dp)
+ .fillMaxWidth()
+ )
+ }
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(16.dp)
+ ) {
+ server.icon?.let {
+ RemoteImage(
+ url = "$REVOLT_FILES/icons/${it.id}/server.png?max_side=256",
+ description = null,
+ modifier = Modifier
+ .clip(CircleShape)
+ .height(48.dp)
+ .width(48.dp),
+ contentScale = ContentScale.Crop
+ )
+ } ?: run {
+ IconPlaceholder(
+ name = server.name ?: stringResource(R.string.unknown),
+ modifier = Modifier
+ .clip(CircleShape)
+ .height(48.dp)
+ .width(48.dp)
+ )
+ }
+
+ Spacer(modifier = Modifier.width(12.dp))
+
+ CompositionLocalProvider(LocalContentColor provides Color.White) {
+ if (server.flags has ServerFlags.Official) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_revolt_decagram_24dp),
+ contentDescription = stringResource(R.string.server_flag_official),
+ modifier = Modifier
+ .padding(end = 8.dp)
+ .size(24.dp)
+ )
+ }
+ if (server.flags has ServerFlags.Verified) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_check_decagram_24dp),
+ contentDescription = stringResource(R.string.server_flag_verified),
+ modifier = Modifier
+ .padding(end = 8.dp)
+ .size(24.dp)
+ )
+ }
+
+ Text(
+ text = server.name ?: stringResource(R.string.unknown),
+ style = MaterialTheme.typography.labelLarge,
+ fontSize = 16.sp
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt
index 50336fd6..3885d95f 100644
--- a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt
+++ b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt
@@ -94,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.ReportServerDialog
import chat.revolt.screens.chat.dialogs.safety.ReportUserDialog
import chat.revolt.screens.chat.views.FriendsScreen
import chat.revolt.screens.chat.views.HomeScreen
@@ -590,6 +591,9 @@ fun ChatRouterScreen(
onHideSheet = {
serverContextSheetState.hide()
showServerContextSheet = false
+ },
+ onReportServer = {
+ navController.navigate("report/server/${serverContextSheetTarget}")
}
)
}
@@ -1088,6 +1092,16 @@ fun ChannelNavigator(
}
}
+ dialog("report/server/{serverId}") { backStackEntry ->
+ val serverId = backStackEntry.arguments?.getString("serverId")
+ if (serverId != null) {
+ ReportServerDialog(
+ navController = navController,
+ serverId = serverId
+ )
+ }
+ }
+
dialog("report/user/{userId}") { backStackEntry ->
val userId = backStackEntry.arguments?.getString("userId")
if (userId != null) {
diff --git a/app/src/main/java/chat/revolt/screens/chat/dialogs/safety/ReportServerDialog.kt b/app/src/main/java/chat/revolt/screens/chat/dialogs/safety/ReportServerDialog.kt
new file mode 100644
index 00000000..98127255
--- /dev/null
+++ b/app/src/main/java/chat/revolt/screens/chat/dialogs/safety/ReportServerDialog.kt
@@ -0,0 +1,319 @@
+package chat.revolt.screens.chat.dialogs.safety
+
+import android.util.Log
+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.material.icons.Icons
+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.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.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+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.putServerReport
+import chat.revolt.api.schemas.ContentReportReason
+import chat.revolt.components.generic.FormTextField
+import chat.revolt.components.screens.settings.ServerOverview
+import kotlinx.coroutines.launch
+
+enum class ServerReportFlowState {
+ Reason,
+ Sending,
+ Done,
+ Error
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ReportServerDialog(navController: NavController, serverId: String) {
+ val server = RevoltAPI.serverCache[serverId]
+ if (server == null) {
+ navController.popBackStack()
+ return
+ }
+
+ val state = remember { mutableStateOf(ServerReportFlowState.Reason) }
+
+ val selectedReason = remember { mutableStateOf("Illegal") }
+ val userAddedContext = remember { mutableStateOf("") }
+
+ when (state.value) {
+ ServerReportFlowState.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),
+ "NoneSpecified" to stringResource(id = R.string.report_reason_content_other)
+ )
+ val reasonDropdownExpanded = remember { mutableStateOf(false) }
+
+ AlertDialog(
+ onDismissRequest = {
+ // nothing - prevent mistaps from closing the dialog
+ },
+ title = {
+ Text(
+ text = stringResource(id = R.string.report_server_heading),
+ modifier = Modifier.fillMaxWidth()
+ )
+ },
+ text = {
+ Column {
+ Text(text = stringResource(id = R.string.report_server))
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(
+ text = stringResource(id = R.string.report_server_preview),
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+
+ ServerOverview(server)
+
+ 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 = ServerReportFlowState.Sending
+ },
+ modifier = Modifier.testTag("report_send")
+ ) {
+ Text(text = stringResource(id = R.string.report_submit))
+ }
+ }
+ )
+ }
+
+ ServerReportFlowState.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("ReportServerDialog", "Reporting server $serverId")
+ putServerReport(
+ serverId,
+ ContentReportReason.valueOf(selectedReason.value),
+ userAddedContext.value
+ )
+ state.value = ServerReportFlowState.Done
+ } catch (e: Error) {
+ state.value = ServerReportFlowState.Error
+ Log.e("ReportServerDialog", "Failed to report server", e)
+ return@launch
+ }
+ }
+ }
+ }
+ },
+ dismissButton = {},
+ confirmButton = {}
+ )
+ }
+
+ ServerReportFlowState.Done -> {
+ 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
+ )
+ }
+ }
+ },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ navController.popBackStack()
+ },
+ modifier = Modifier.testTag("report_close")
+ ) {
+ Text(text = stringResource(id = R.string.report_submit_close))
+ }
+ }
+ )
+ }
+
+ ServerReportFlowState.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 = {}
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/chat/revolt/sheets/ServerContextSheet.kt b/app/src/main/java/chat/revolt/sheets/ServerContextSheet.kt
index 46ce1eff..726cbc78 100644
--- a/app/src/main/java/chat/revolt/sheets/ServerContextSheet.kt
+++ b/app/src/main/java/chat/revolt/sheets/ServerContextSheet.kt
@@ -1,28 +1,21 @@
package chat.revolt.sheets
import android.widget.Toast
-import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
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.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
-import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -30,32 +23,27 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Brush
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
import chat.revolt.R
-import chat.revolt.api.REVOLT_FILES
import chat.revolt.api.RevoltAPI
import chat.revolt.api.routes.server.leaveOrDeleteServer
-import chat.revolt.api.schemas.ServerFlags
-import chat.revolt.api.schemas.has
-import chat.revolt.components.generic.IconPlaceholder
-import chat.revolt.components.generic.RemoteImage
import chat.revolt.components.generic.SheetClickable
import chat.revolt.components.generic.UIMarkdown
+import chat.revolt.components.screens.settings.ServerOverview
import chat.revolt.internals.Platform
import kotlinx.coroutines.launch
@Composable
-fun ServerContextSheet(serverId: String, onHideSheet: suspend () -> Unit) {
+fun ServerContextSheet(
+ serverId: String,
+ onReportServer: () -> Unit,
+ onHideSheet: suspend () -> Unit
+) {
val server = RevoltAPI.serverCache[serverId]
if (server == null) {
@@ -152,98 +140,7 @@ fun ServerContextSheet(serverId: String, onHideSheet: suspend () -> Unit) {
Column(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 0.dp, bottom = 16.dp)
) {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .clip(MaterialTheme.shapes.large),
- contentAlignment = Alignment.BottomStart
- ) {
- server.banner?.let {
- Box(
- modifier = Modifier
- .background(Color.Black.copy(alpha = 0.25f))
- .height(166.dp)
- .fillMaxWidth()
- )
-
- RemoteImage(
- url = "$REVOLT_FILES/banners/${it.id}",
- description = null,
- modifier = Modifier
- .height(166.dp)
- .fillMaxWidth(),
- contentScale = ContentScale.FillWidth
- )
-
- Box(
- modifier = Modifier
- .background(
- Brush.verticalGradient(
- listOf(
- Color.Transparent,
- Color.Black.copy(alpha = 0.7f)
- )
- )
- )
- .height(166.dp)
- .fillMaxWidth()
- )
- }
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.padding(16.dp)
- ) {
- server.icon?.let {
- RemoteImage(
- url = "$REVOLT_FILES/icons/${it.id}/server.png?max_side=256",
- description = null,
- modifier = Modifier
- .clip(CircleShape)
- .height(48.dp)
- .width(48.dp),
- contentScale = ContentScale.Crop
- )
- } ?: run {
- IconPlaceholder(
- name = server.name ?: stringResource(R.string.unknown),
- modifier = Modifier
- .clip(CircleShape)
- .height(48.dp)
- .width(48.dp)
- )
- }
-
- Spacer(modifier = Modifier.width(12.dp))
-
- CompositionLocalProvider(LocalContentColor provides Color.White) {
- if (server.flags has ServerFlags.Official) {
- Icon(
- painter = painterResource(id = R.drawable.ic_revolt_decagram_24dp),
- contentDescription = stringResource(R.string.server_flag_official),
- modifier = Modifier
- .padding(end = 8.dp)
- .size(24.dp)
- )
- }
- if (server.flags has ServerFlags.Verified) {
- Icon(
- painter = painterResource(id = R.drawable.ic_check_decagram_24dp),
- contentDescription = stringResource(R.string.server_flag_verified),
- modifier = Modifier
- .padding(end = 8.dp)
- .size(24.dp)
- )
- }
-
- Text(
- text = server.name ?: stringResource(R.string.unknown),
- style = MaterialTheme.typography.labelLarge,
- fontSize = 16.sp
- )
- }
- }
- }
+ ServerOverview(server)
Column(
modifier = Modifier.padding(horizontal = 4.dp)
@@ -321,6 +218,21 @@ fun ServerContextSheet(serverId: String, onHideSheet: suspend () -> Unit) {
}
if (server.owner != RevoltAPI.selfId) {
+ SheetClickable(icon = {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_flag_24dp),
+ contentDescription = null,
+ modifier = it
+ )
+ }, label = {
+ Text(
+ text = stringResource(id = R.string.server_context_sheet_actions_report),
+ style = it
+ )
+ }, dangerous = true) {
+ onReportServer()
+ }
+
SheetClickable(icon = {
Icon(
painter = painterResource(id = R.drawable.ic_arrow_left_bold_box_24dp),
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5e7f12ad..ad38c977 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -295,6 +295,7 @@
Leave
Stay
Leave Silently
+ Report
Can\'t resolve this user
This user may have been deleted or you may not have permission to view them.