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.