feat: handling for platform mod
Signed-off-by: Infi <wingit@geist.ga>
This commit is contained in:
parent
38406388b8
commit
b26b8bad47
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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 ->
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue