From b26b8bad47f22ed5a8c5ac7091541c71da2aacac Mon Sep 17 00:00:00 2001 From: Infi Date: Wed, 2 Aug 2023 19:28:47 +0200 Subject: [PATCH] feat: handling for platform mod Signed-off-by: Infi --- .../revolt/api/internals/DirectMessages.kt | 34 ++++++ .../chat/revolt/api/internals/SpecialUsers.kt | 10 ++ .../revolt/components/chat/MessageField.kt | 72 +++++++------ .../revolt/screens/chat/ChatRouterScreen.kt | 101 ++++++++++++++++++ .../chat/views/channel/ChannelScreen.kt | 2 + .../views/channel/ChannelScreenViewModel.kt | 30 ++++++ app/src/main/res/values/strings.xml | 10 ++ 7 files changed, 229 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/chat/revolt/api/internals/DirectMessages.kt create mode 100644 app/src/main/java/chat/revolt/api/internals/SpecialUsers.kt diff --git a/app/src/main/java/chat/revolt/api/internals/DirectMessages.kt b/app/src/main/java/chat/revolt/api/internals/DirectMessages.kt new file mode 100644 index 00000000..42a1d15c --- /dev/null +++ b/app/src/main/java/chat/revolt/api/internals/DirectMessages.kt @@ -0,0 +1,34 @@ +package chat.revolt.api.internals + +import chat.revolt.api.RevoltAPI +import chat.revolt.api.internals.SpecialUsers.PLATFORM_MODERATION_USER +import chat.revolt.api.schemas.Channel +import chat.revolt.api.schemas.ChannelType + +object DirectMessages { + fun unreadDMs(): List { + return RevoltAPI.channelCache.values + .filter { + it.channelType in listOf( + ChannelType.DirectMessage, ChannelType.Group + ) && it.active == true && it.lastMessageID != null + } + .filter { + it.id?.let { id -> RevoltAPI.unreads.hasUnread(id, it.lastMessageID!!) } ?: false + } + } + + fun hasPlatformModerationDM(): Boolean { + return unreadDMs().any { + it.channelType == ChannelType.DirectMessage && + it.recipients?.contains(PLATFORM_MODERATION_USER) ?: false + } + } + + fun getPlatformModerationDM(): Channel? { + return unreadDMs().firstOrNull { + it.channelType == ChannelType.DirectMessage && + it.recipients?.contains(PLATFORM_MODERATION_USER) ?: false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/internals/SpecialUsers.kt b/app/src/main/java/chat/revolt/api/internals/SpecialUsers.kt new file mode 100644 index 00000000..33bca118 --- /dev/null +++ b/app/src/main/java/chat/revolt/api/internals/SpecialUsers.kt @@ -0,0 +1,10 @@ +package chat.revolt.api.internals + +object SpecialUsers { + val PLATFORM_MODERATION_USER = "01FC17E1WTM2BGE4F3ARN3FDAF" + + val TRUSTED_MODERATION_BOTS = listOf( + "01GXBYCNQ52A9QYCQ99RBPXPAW", // AutoMod + "01FCXRNNVW69AMSHBE61W1M5T3", // AutoMod Nightly + ) +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/components/chat/MessageField.kt b/app/src/main/java/chat/revolt/components/chat/MessageField.kt index fcf2ef9b..6c1e4886 100644 --- a/app/src/main/java/chat/revolt/components/chat/MessageField.kt +++ b/app/src/main/java/chat/revolt/components/chat/MessageField.kt @@ -61,6 +61,8 @@ fun MessageField( forceSendButton: Boolean = false, disabled: Boolean = false, editMode: Boolean = false, + denied: Boolean = false, + denyReason: String? = null, cancelEdit: () -> Unit = {}, onFocusChange: (Boolean) -> Unit = {}, ) { @@ -73,7 +75,7 @@ fun MessageField( ChannelType.SavedMessages -> R.string.message_field_placeholder_notes } - val sendButtonVisible = (messageContent.isNotBlank() || forceSendButton) && !disabled + val sendButtonVisible = (messageContent.isNotBlank() || forceSendButton) && !disabled && !denied Row( modifier = Modifier @@ -84,7 +86,7 @@ fun MessageField( value = messageContent, onValueChange = onMessageContentChange, singleLine = false, - enabled = !disabled, + enabled = !disabled && !denied, textStyle = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onSurface), cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), modifier = modifier @@ -104,14 +106,22 @@ fun MessageField( visualTransformation = VisualTransformation.None, interactionSource = remember { MutableInteractionSource() }, placeholder = { - Text( - text = stringResource( - id = placeholderResource, - channelName - ), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + if (denied) { + Text( + text = denyReason + ?: stringResource(R.string.message_field_denied_generic), + overflow = TextOverflow.Ellipsis, + ) + } else { + Text( + text = stringResource( + id = placeholderResource, + channelName + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } }, colors = TextFieldDefaults.colors( focusedIndicatorColor = Color.Transparent, @@ -127,28 +137,30 @@ fun MessageField( ), contentPadding = PaddingValues(16.dp), leadingIcon = { - Icon( - when { - editMode -> Icons.Default.Close - else -> Icons.Default.Add - }, - tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), - contentDescription = stringResource(id = R.string.add_attachment_alt), - modifier = Modifier - .clip(CircleShape) - .size(32.dp) - .clickable { - when { - editMode -> cancelEdit() - else -> { - focusRequester.freeFocus() // hide keyboard because it's annoying - onAddAttachment() + if (!denied) { + Icon( + when { + editMode -> Icons.Default.Close + else -> Icons.Default.Add + }, + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), + contentDescription = stringResource(id = R.string.add_attachment_alt), + modifier = Modifier + .clip(CircleShape) + .size(32.dp) + .clickable { + when { + editMode -> cancelEdit() + else -> { + focusRequester.freeFocus() // hide keyboard because it's annoying + onAddAttachment() + } } } - } - .padding(4.dp) - .testTag("add_attachment") - ) + .padding(4.dp) + .testTag("add_attachment") + ) + } }, trailingIcon = { AnimatedVisibility(sendButtonVisible, 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 609f6548..cf6fdbc4 100644 --- a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt @@ -52,12 +52,16 @@ import androidx.navigation.compose.dialog import androidx.navigation.compose.rememberNavController import chat.revolt.R import chat.revolt.api.RevoltAPI +import chat.revolt.api.internals.ChannelUtils +import chat.revolt.api.internals.DirectMessages import chat.revolt.api.realtime.DisconnectionState import chat.revolt.api.realtime.RealtimeSocket import chat.revolt.api.routes.server.fetchMembers +import chat.revolt.api.schemas.ChannelType import chat.revolt.api.schemas.User import chat.revolt.api.settings.SyncedSettings import chat.revolt.components.chat.DisconnectedNotice +import chat.revolt.components.generic.GroupIcon import chat.revolt.components.generic.UserAvatar import chat.revolt.components.generic.presenceFromStatus import chat.revolt.components.screens.chat.drawer.channel.ChannelList @@ -190,6 +194,8 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil composition = sidebarSparkComposition, ) + var showPlatformModDMHint by remember { mutableStateOf(false) } + var showStatusSheet by remember { mutableStateOf(false) } var showAddServerSheet by remember { mutableStateOf(false) } @@ -254,6 +260,16 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil } } + LaunchedEffect(DirectMessages.unreadDMs()) { + snapshotFlow { DirectMessages.unreadDMs() } + .distinctUntilChanged() + .collect { _ -> + if (DirectMessages.hasPlatformModerationDM()) { + showPlatformModDMHint = true + } + } + } + if (showSidebarSpark.value) { AlertDialog( onDismissRequest = {}, @@ -287,6 +303,29 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil ) } + if (showPlatformModDMHint) { + AlertDialog( + onDismissRequest = {}, + title = { + Text(stringResource(id = R.string.notice_platform_mod_dm_title)) + }, + text = { + Text(stringResource(id = R.string.notice_platform_mod_dm_description)) + }, + confirmButton = { + TextButton(onClick = { + showPlatformModDMHint = false + DirectMessages.getPlatformModerationDM()?.id?.let { + viewModel.navigateToServer("home", navController) + viewModel.navigateToChannel(it, navController) + } + }) { + Text(stringResource(id = R.string.notice_platform_mod_dm_acknowledge)) + } + } + ) + } + if (showStatusSheet) { val statusSheetState = rememberModalBottomSheetState() @@ -409,6 +448,68 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil .size(48.dp) ) + DirectMessages.unreadDMs().forEach { + when (it.channelType) { + ChannelType.Group -> GroupIcon( + name = it.name ?: "?", + size = 48.dp, + onClick = { + it.id?.let { id -> + viewModel.navigateToServer( + "home", + navController + ) + viewModel.navigateToChannel( + id, + navController + ) + } + }, + icon = it.icon, + modifier = Modifier + .padding(8.dp) + .size(48.dp) + ) + + else -> { + val partner = + if (it.channelType == ChannelType.DirectMessage) RevoltAPI.userCache[ChannelUtils.resolveDMPartner( + it + )] else null + + UserAvatar( + username = partner?.let { p -> + User.resolveDefaultName( + p + ) + } ?: it.name ?: "?", + presence = presenceFromStatus( + partner?.status?.presence ?: "" + ), + userId = partner?.id ?: it.id ?: "", + avatar = partner?.avatar ?: it.icon, + size = 48.dp, + presenceSize = 16.dp, + onClick = { + it.id?.let { id -> + viewModel.navigateToServer( + "home", + navController + ) + viewModel.navigateToChannel( + id, + navController + ) + } + }, + modifier = Modifier + .padding(8.dp) + .size(48.dp) + ) + } + } + } + ServerDrawerSeparator() // This seems to confuse the formatter, here's what it does: diff --git a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt index df0ea7de..36fcd9c2 100644 --- a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt @@ -470,6 +470,8 @@ fun ChannelScreen( ), forceSendButton = viewModel.pendingAttachments.isNotEmpty(), disabled = viewModel.pendingAttachments.isNotEmpty() && viewModel.isSendingMessage, + denied = viewModel.denyMessageField, + denyReason = stringResource(id = viewModel.denyMessageFieldReasonResource), editMode = viewModel.editingMessage != null, cancelEdit = viewModel::cancelEditingMessage, onFocusChange = { nowFocused -> diff --git a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt index 62c399be..3746d48c 100644 --- a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt +++ b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt @@ -3,14 +3,18 @@ package chat.revolt.screens.chat.views.channel import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.runtime.toMutableStateList import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import chat.revolt.R import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltJson +import chat.revolt.api.internals.ChannelUtils +import chat.revolt.api.internals.SpecialUsers import chat.revolt.api.internals.ULID import chat.revolt.api.realtime.RealtimeSocketFrames import chat.revolt.api.realtime.frames.receivable.ChannelStartTypingFrame @@ -66,6 +70,9 @@ class ChannelScreenViewModel : ViewModel() { var editingMessage by mutableStateOf(null) + var denyMessageField by mutableStateOf(false) + var denyMessageFieldReasonResource by mutableIntStateOf(R.string.message_field_denied_generic) + private fun popAttachmentBatch() { pendingAttachments = pendingAttachments.drop(MAX_ATTACHMENTS_PER_MESSAGE).toMutableStateList() @@ -152,6 +159,8 @@ class ChannelScreenViewModel : ViewModel() { activeChannel = RevoltAPI.channelCache[id] } + checkShouldDenyMessageField() + if (activeChannel?.lastMessageID != null) { ackNewest() } else { @@ -409,4 +418,25 @@ class ChannelScreenViewModel : ViewModel() { editingMessage = null pendingMessageContent = "" } + + private fun checkShouldDenyMessageField() { + // TODO Check for send message permission. + val hasPermission = true + + if (activeChannel == null) return + + val partnerId = ChannelUtils.resolveDMPartner(activeChannel!!) ?: return + + denyMessageField = when { + partnerId == SpecialUsers.PLATFORM_MODERATION_USER -> true + !hasPermission -> true + else -> false + } + + denyMessageFieldReasonResource = when { + partnerId == SpecialUsers.PLATFORM_MODERATION_USER -> R.string.message_field_denied_platform_moderation + !hasPermission -> R.string.message_field_denied_no_permission + else -> R.string.message_field_denied_generic + } + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 59e07fbf..67efa1c7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -134,6 +134,12 @@ Message %1$s Add a note + Verified moderation bot + + You can\'t send messages in this channel. + You don\'t have permission to send messages in this channel. + This is a trusted channel for notices from our moderation team. You can\'t send messages here. + Unknown message, tap to jump Sent attachments @@ -297,6 +303,10 @@ Then long tap your profile picture to open the settings. Got it + Important notice regarding your account + You have received an important notice regarding your account from our moderation team. Please read it carefully. + View + General Miscellaneous Revolt v%1$s