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,
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,

View File

@ -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:

View File

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

View File

@ -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<String?>(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
}
}
}

View File

@ -134,6 +134,12 @@
<string name="message_field_placeholder_group">Message %1$s</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_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_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_miscellaneous">Miscellaneous</string>
<string name="settings_category_last" translatable="false">Revolt v%1$s</string>