feat: handling for platform mod

Signed-off-by: Infi <wingit@geist.ga>
This commit is contained in:
Infi 2023-08-02 19:28:47 +02:00
parent 38406388b8
commit b26b8bad47
7 changed files with 229 additions and 30 deletions

View File

@ -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<Channel> {
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
}
}
}

View File

@ -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
)
}

View File

@ -61,6 +61,8 @@ fun MessageField(
forceSendButton: Boolean = false, forceSendButton: Boolean = false,
disabled: Boolean = false, disabled: Boolean = false,
editMode: Boolean = false, editMode: Boolean = false,
denied: Boolean = false,
denyReason: String? = null,
cancelEdit: () -> Unit = {}, cancelEdit: () -> Unit = {},
onFocusChange: (Boolean) -> Unit = {}, onFocusChange: (Boolean) -> Unit = {},
) { ) {
@ -73,7 +75,7 @@ fun MessageField(
ChannelType.SavedMessages -> R.string.message_field_placeholder_notes ChannelType.SavedMessages -> R.string.message_field_placeholder_notes
} }
val sendButtonVisible = (messageContent.isNotBlank() || forceSendButton) && !disabled val sendButtonVisible = (messageContent.isNotBlank() || forceSendButton) && !disabled && !denied
Row( Row(
modifier = Modifier modifier = Modifier
@ -84,7 +86,7 @@ fun MessageField(
value = messageContent, value = messageContent,
onValueChange = onMessageContentChange, onValueChange = onMessageContentChange,
singleLine = false, singleLine = false,
enabled = !disabled, enabled = !disabled && !denied,
textStyle = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onSurface), textStyle = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onSurface),
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
modifier = modifier modifier = modifier
@ -104,14 +106,22 @@ fun MessageField(
visualTransformation = VisualTransformation.None, visualTransformation = VisualTransformation.None,
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
placeholder = { placeholder = {
Text( if (denied) {
text = stringResource( Text(
id = placeholderResource, text = denyReason
channelName ?: stringResource(R.string.message_field_denied_generic),
), overflow = TextOverflow.Ellipsis,
maxLines = 1, )
overflow = TextOverflow.Ellipsis, } else {
) Text(
text = stringResource(
id = placeholderResource,
channelName
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}, },
colors = TextFieldDefaults.colors( colors = TextFieldDefaults.colors(
focusedIndicatorColor = Color.Transparent, focusedIndicatorColor = Color.Transparent,
@ -127,28 +137,30 @@ fun MessageField(
), ),
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
leadingIcon = { leadingIcon = {
Icon( if (!denied) {
when { Icon(
editMode -> Icons.Default.Close when {
else -> Icons.Default.Add editMode -> Icons.Default.Close
}, else -> Icons.Default.Add
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), },
contentDescription = stringResource(id = R.string.add_attachment_alt), tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
modifier = Modifier contentDescription = stringResource(id = R.string.add_attachment_alt),
.clip(CircleShape) modifier = Modifier
.size(32.dp) .clip(CircleShape)
.clickable { .size(32.dp)
when { .clickable {
editMode -> cancelEdit() when {
else -> { editMode -> cancelEdit()
focusRequester.freeFocus() // hide keyboard because it's annoying else -> {
onAddAttachment() focusRequester.freeFocus() // hide keyboard because it's annoying
onAddAttachment()
}
} }
} }
} .padding(4.dp)
.padding(4.dp) .testTag("add_attachment")
.testTag("add_attachment") )
) }
}, },
trailingIcon = { trailingIcon = {
AnimatedVisibility(sendButtonVisible, AnimatedVisibility(sendButtonVisible,

View File

@ -52,12 +52,16 @@ import androidx.navigation.compose.dialog
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import chat.revolt.R import chat.revolt.R
import chat.revolt.api.RevoltAPI 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.DisconnectionState
import chat.revolt.api.realtime.RealtimeSocket import chat.revolt.api.realtime.RealtimeSocket
import chat.revolt.api.routes.server.fetchMembers import chat.revolt.api.routes.server.fetchMembers
import chat.revolt.api.schemas.ChannelType
import chat.revolt.api.schemas.User import chat.revolt.api.schemas.User
import chat.revolt.api.settings.SyncedSettings import chat.revolt.api.settings.SyncedSettings
import chat.revolt.components.chat.DisconnectedNotice import chat.revolt.components.chat.DisconnectedNotice
import chat.revolt.components.generic.GroupIcon
import chat.revolt.components.generic.UserAvatar import chat.revolt.components.generic.UserAvatar
import chat.revolt.components.generic.presenceFromStatus import chat.revolt.components.generic.presenceFromStatus
import chat.revolt.components.screens.chat.drawer.channel.ChannelList import chat.revolt.components.screens.chat.drawer.channel.ChannelList
@ -190,6 +194,8 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil
composition = sidebarSparkComposition, composition = sidebarSparkComposition,
) )
var showPlatformModDMHint by remember { mutableStateOf(false) }
var showStatusSheet by remember { mutableStateOf(false) } var showStatusSheet by remember { mutableStateOf(false) }
var showAddServerSheet 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) { if (showSidebarSpark.value) {
AlertDialog( AlertDialog(
onDismissRequest = {}, 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) { if (showStatusSheet) {
val statusSheetState = rememberModalBottomSheetState() val statusSheetState = rememberModalBottomSheetState()
@ -409,6 +448,68 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil
.size(48.dp) .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() ServerDrawerSeparator()
// This seems to confuse the formatter, here's what it does: // This seems to confuse the formatter, here's what it does:

View File

@ -470,6 +470,8 @@ fun ChannelScreen(
), ),
forceSendButton = viewModel.pendingAttachments.isNotEmpty(), forceSendButton = viewModel.pendingAttachments.isNotEmpty(),
disabled = viewModel.pendingAttachments.isNotEmpty() && viewModel.isSendingMessage, disabled = viewModel.pendingAttachments.isNotEmpty() && viewModel.isSendingMessage,
denied = viewModel.denyMessageField,
denyReason = stringResource(id = viewModel.denyMessageFieldReasonResource),
editMode = viewModel.editingMessage != null, editMode = viewModel.editingMessage != null,
cancelEdit = viewModel::cancelEditingMessage, cancelEdit = viewModel::cancelEditingMessage,
onFocusChange = { nowFocused -> onFocusChange = { nowFocused ->

View File

@ -3,14 +3,18 @@ package chat.revolt.screens.chat.views.channel
import android.util.Log import android.util.Log
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import chat.revolt.R
import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltAPI
import chat.revolt.api.RevoltJson 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.internals.ULID
import chat.revolt.api.realtime.RealtimeSocketFrames import chat.revolt.api.realtime.RealtimeSocketFrames
import chat.revolt.api.realtime.frames.receivable.ChannelStartTypingFrame import chat.revolt.api.realtime.frames.receivable.ChannelStartTypingFrame
@ -66,6 +70,9 @@ class ChannelScreenViewModel : ViewModel() {
var editingMessage by mutableStateOf<String?>(null) var editingMessage by mutableStateOf<String?>(null)
var denyMessageField by mutableStateOf(false)
var denyMessageFieldReasonResource by mutableIntStateOf(R.string.message_field_denied_generic)
private fun popAttachmentBatch() { private fun popAttachmentBatch() {
pendingAttachments = pendingAttachments =
pendingAttachments.drop(MAX_ATTACHMENTS_PER_MESSAGE).toMutableStateList() pendingAttachments.drop(MAX_ATTACHMENTS_PER_MESSAGE).toMutableStateList()
@ -152,6 +159,8 @@ class ChannelScreenViewModel : ViewModel() {
activeChannel = RevoltAPI.channelCache[id] activeChannel = RevoltAPI.channelCache[id]
} }
checkShouldDenyMessageField()
if (activeChannel?.lastMessageID != null) { if (activeChannel?.lastMessageID != null) {
ackNewest() ackNewest()
} else { } else {
@ -409,4 +418,25 @@ class ChannelScreenViewModel : ViewModel() {
editingMessage = null editingMessage = null
pendingMessageContent = "" 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
}
}
} }

View File

@ -134,6 +134,12 @@
<string name="message_field_placeholder_group">Message %1$s</string> <string name="message_field_placeholder_group">Message %1$s</string>
<string name="message_field_placeholder_notes">Add a note</string> <string name="message_field_placeholder_notes">Add a note</string>
<string name="message_field_decoration_trusted_moderation_bot">Verified moderation bot</string>
<string name="message_field_denied_generic">You can\'t send messages in this channel.</string>
<string name="message_field_denied_no_permission">You don\'t have permission to send messages in this channel.</string>
<string name="message_field_denied_platform_moderation">This is a trusted channel for notices from our moderation team. You can\'t send messages here.</string>
<string name="reply_message_not_cached">Unknown message, tap to jump</string> <string name="reply_message_not_cached">Unknown message, tap to jump</string>
<string name="reply_message_empty_has_attachments">Sent attachments</string> <string name="reply_message_empty_has_attachments">Sent attachments</string>
@ -297,6 +303,10 @@
<string name="spark_sidebar_settings_tutorial_description_2">Then long tap your profile picture to open the settings.</string> <string name="spark_sidebar_settings_tutorial_description_2">Then long tap your profile picture to open the settings.</string>
<string name="spark_sidebar_settings_tutorial_acknowledge">Got it</string> <string name="spark_sidebar_settings_tutorial_acknowledge">Got it</string>
<string name="notice_platform_mod_dm_title">Important notice regarding your account</string>
<string name="notice_platform_mod_dm_description">You have received an important notice regarding your account from our moderation team. Please read it carefully.</string>
<string name="notice_platform_mod_dm_acknowledge">View</string>
<string name="settings_category_general">General</string> <string name="settings_category_general">General</string>
<string name="settings_category_miscellaneous">Miscellaneous</string> <string name="settings_category_miscellaneous">Miscellaneous</string>
<string name="settings_category_last" translatable="false">Revolt v%1$s</string> <string name="settings_category_last" translatable="false">Revolt v%1$s</string>