feat: support webhooks

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2024-11-05 02:01:15 +01:00
parent 197c6db56a
commit 7e2eccf387
9 changed files with 150 additions and 10 deletions

View File

@ -20,6 +20,7 @@ data class Message(
val mentions: List<String>? = null, val mentions: List<String>? = null,
val masquerade: Masquerade? = null, val masquerade: Masquerade? = null,
val system: SystemInfo? = null, val system: SystemInfo? = null,
val webhook: WebHook? = null,
val type: String? = null, // this is _only_ used for websocket events! val type: String? = null, // this is _only_ used for websocket events!
val tail: Boolean? = null // this is used to determine if the message is the last in a message group val tail: Boolean? = null // this is used to determine if the message is the last in a message group
) { ) {
@ -110,3 +111,9 @@ data class SystemInfo(
val to: String? = null, val to: String? = null,
val content: String? = null val content: String? = null
) )
@Serializable
data class WebHook(
val avatar: String? = null,
val name: String? = null,
)

View File

@ -14,6 +14,7 @@ sealed class Action {
data class ReportUser(val userId: String) : Action() data class ReportUser(val userId: String) : Action()
data class ReportMessage(val messageId: String) : Action() data class ReportMessage(val messageId: String) : Action()
data class OpenVoiceChannelOverlay(val channelId: String) : Action() data class OpenVoiceChannelOverlay(val channelId: String) : Action()
data object OpenWebhookSheet : Action()
} }
val ActionChannel = Channel<Action>( val ActionChannel = Channel<Action>(

View File

@ -13,7 +13,8 @@ enum class InlineBadge {
Bot, Bot,
Bridge, Bridge,
PlatformModeration, PlatformModeration,
TeamMember TeamMember,
Webhook
} }
@Composable @Composable
@ -50,6 +51,13 @@ fun InlineBadge(
tint = colour, tint = colour,
modifier = modifier modifier = modifier
) )
InlineBadge.Webhook -> Icon(
painter = painterResource(id = R.drawable.ic_hook_24dp),
contentDescription = stringResource(id = R.string.badge_webhook_alt),
tint = colour,
modifier = modifier
)
} }
} }
@ -60,6 +68,7 @@ fun InlineBadges(
bridge: Boolean = false, bridge: Boolean = false,
platformModeration: Boolean = false, platformModeration: Boolean = false,
teamMember: Boolean = false, teamMember: Boolean = false,
webhook: Boolean = false,
colour: Color = Color.Unspecified, colour: Color = Color.Unspecified,
precedingIfAny: @Composable () -> Unit = {}, precedingIfAny: @Composable () -> Unit = {},
followingIfAny: @Composable () -> Unit = {} followingIfAny: @Composable () -> Unit = {}
@ -99,6 +108,13 @@ fun InlineBadges(
colour = colour colour = colour
) )
} }
if (webhook) {
InlineBadge(
badge = InlineBadge.Webhook,
modifier = modifier,
colour = colour
)
}
} }
if (hasBadges) { if (hasBadges) {

View File

@ -180,6 +180,8 @@ fun Message(
canReply: Boolean = false, canReply: Boolean = false,
onReply: () -> Unit = {}, onReply: () -> Unit = {},
onAddReaction: () -> Unit = {}, onAddReaction: () -> Unit = {},
fromWebhook: Boolean = false,
webhookName: String? = null
) { ) {
val author = RevoltAPI.userCache[message.author] ?: return CircularProgressIndicator() val author = RevoltAPI.userCache[message.author] ?: return CircularProgressIndicator()
val context = LocalContext.current val context = LocalContext.current
@ -304,7 +306,7 @@ fun Message(
if (message.tail == false) { if (message.tail == false) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
text = authorName(message), text = webhookName ?: authorName(message),
style = LocalTextStyle.current.copy( style = LocalTextStyle.current.copy(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
brush = authorColour(message) brush = authorColour(message)
@ -328,6 +330,7 @@ fun Message(
bridge = message.masquerade != null && author.bot != null, bridge = message.masquerade != null && author.bot != null,
platformModeration = author.id == SpecialUsers.PLATFORM_MODERATION_USER, platformModeration = author.id == SpecialUsers.PLATFORM_MODERATION_USER,
teamMember = author.id in SpecialUsers.TEAM_MEMBER_FLAIRS.keys, teamMember = author.id in SpecialUsers.TEAM_MEMBER_FLAIRS.keys,
webhook = fromWebhook,
colour = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f), colour = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f),
modifier = Modifier.size(16.dp), modifier = Modifier.size(16.dp),
precedingIfAny = { precedingIfAny = {

View File

@ -78,6 +78,7 @@ import chat.revolt.sheets.ReactionInfoSheet
import chat.revolt.sheets.ServerContextSheet import chat.revolt.sheets.ServerContextSheet
import chat.revolt.sheets.StatusSheet import chat.revolt.sheets.StatusSheet
import chat.revolt.sheets.UserInfoSheet import chat.revolt.sheets.UserInfoSheet
import chat.revolt.sheets.WebHookUserSheet
import com.airbnb.lottie.RenderMode import com.airbnb.lottie.RenderMode
import com.airbnb.lottie.compose.LottieAnimation import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.LottieCompositionSpec
@ -229,6 +230,8 @@ fun ChatRouterScreen(
var userContextSheetTarget by remember { mutableStateOf("") } var userContextSheetTarget by remember { mutableStateOf("") }
var userContextSheetServer by remember { mutableStateOf<String?>(null) } var userContextSheetServer by remember { mutableStateOf<String?>(null) }
var showWebhookInfoSheet by remember { mutableStateOf(false) }
var showChannelUnavailableAlert by remember { mutableStateOf(false) } var showChannelUnavailableAlert by remember { mutableStateOf(false) }
var showLinkInfoSheet by remember { mutableStateOf(false) } var showLinkInfoSheet by remember { mutableStateOf(false) }
@ -389,6 +392,10 @@ fun ChatRouterScreen(
voiceChannelOverlayChannelId = action.channelId voiceChannelOverlayChannelId = action.channelId
voiceChannelOverlay = true voiceChannelOverlay = true
} }
is Action.OpenWebhookSheet -> {
showWebhookInfoSheet = true
}
} }
} }
} }
@ -554,6 +561,19 @@ fun ChatRouterScreen(
} }
} }
if (showWebhookInfoSheet) {
val webhookInfoSheetState = rememberModalBottomSheetState()
ModalBottomSheet(
sheetState = webhookInfoSheetState,
onDismissRequest = {
showWebhookInfoSheet = false
}
) {
WebHookUserSheet()
}
}
if (showReportUser) { if (showReportUser) {
ReportUserDialog( ReportUserDialog(
onDismiss = { showReportUser = false }, onDismiss = { showReportUser = false },

View File

@ -134,7 +134,6 @@ import kotlinx.coroutines.launch
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import java.io.File import java.io.File
import kotlin.math.max import kotlin.math.max
import kotlin.math.min
sealed class ChannelScreenItem { sealed class ChannelScreenItem {
data class RegularMessage(val message: Message) : ChannelScreenItem() data class RegularMessage(val message: Message) : ChannelScreenItem()
@ -603,14 +602,20 @@ fun ChannelScreen(
} }
}, },
onAvatarClick = { onAvatarClick = {
item.message.author?.let { author -> if (item.message.webhook != null) {
scope.launch { scope.launch {
ActionChannel.send( ActionChannel.send(Action.OpenWebhookSheet)
Action.OpenUserSheet( }
author, } else {
viewModel.channel?.server item.message.author?.let { author ->
scope.launch {
ActionChannel.send(
Action.OpenUserSheet(
author,
viewModel.channel?.server
)
) )
) }
} }
} }
}, },
@ -631,7 +636,9 @@ fun ChannelScreen(
reactSheetTarget = messageId reactSheetTarget = messageId
reactSheetShown = true reactSheetShown = true
} }
} },
fromWebhook = item.message.webhook != null,
webhookName = item.message.webhook?.name
) )
} }

View File

@ -0,0 +1,72 @@
package chat.revolt.sheets
import android.widget.Toast
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.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import chat.revolt.R
import chat.revolt.components.generic.NonIdealState
@Composable
fun WebHookUserSheet(modifier: Modifier = Modifier) {
val context = LocalContext.current
NonIdealState(
icon = {
Icon(
painter = painterResource(R.drawable.ic_hook_24dp),
contentDescription = null,
modifier = Modifier.size(24.dp)
)
},
title = {
Text(
text = stringResource(R.string.user_info_sheet_webhook)
)
},
description = {
Text(
text = stringResource(R.string.user_info_sheet_webhook_description),
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
)
Column(Modifier.padding(horizontal = 16.dp)) {
Text(
text = stringResource(R.string.user_info_sheet_webhook_description_2),
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(32.dp))
Button(
onClick = {
Toast(context).apply {
setText(context.getString(R.string.comingsoon_toast))
duration = Toast.LENGTH_SHORT
show()
}
},
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.user_info_sheet_webhook_learn_more))
}
}
Spacer(Modifier.height(48.dp))
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M18,6C18,7.82 16.76,9.41 15,9.86V17A5,5 0 0,1 10,22A5,5 0 0,1 5,17V12L10,17H7A3,3 0 0,0 10,20A3,3 0 0,0 13,17V9.86C11.23,9.4 10,7.8 10,5.97C10,3.76 11.8,2 14,2C16.22,2 18,3.79 18,6M14,8A2,2 0 0,0 16,6A2,2 0 0,0 14,4A2,2 0 0,0 12,6A2,2 0 0,0 14,8Z" />
</vector>

View File

@ -125,6 +125,7 @@
<string name="badge_masquerade_alt">From linked channel</string> <string name="badge_masquerade_alt">From linked channel</string>
<string name="badge_platform_moderation_alt">Platform moderation</string> <string name="badge_platform_moderation_alt">Platform moderation</string>
<string name="badge_team_member_alt">Revolt team member</string> <string name="badge_team_member_alt">Revolt team member</string>
<string name="badge_webhook_alt">Webhook</string>
<string name="unknown">Unknown</string> <string name="unknown">Unknown</string>
@ -352,6 +353,10 @@
<string name="user_info_sheet_open">Open user info</string> <string name="user_info_sheet_open">Open user info</string>
<string name="user_info_sheet_user_not_found">Can\'t resolve this user</string> <string name="user_info_sheet_user_not_found">Can\'t resolve this user</string>
<string name="user_info_sheet_user_not_found_description">This user may have been deleted or you may not have permission to view them.</string> <string name="user_info_sheet_user_not_found_description">This user may have been deleted or you may not have permission to view them.</string>
<string name="user_info_sheet_webhook">Is it a bird, is it a plane?</string>
<string name="user_info_sheet_webhook_description">No, it\'s a webhook!</string>
<string name="user_info_sheet_webhook_description_2">Webhooks are automated broadcasts from other services on the internet. They can\'t chat back, but you can still see what they have to say.</string>
<string name="user_info_sheet_webhook_learn_more">Learn more</string>
<string name="user_info_sheet_category_bio">Bio</string> <string name="user_info_sheet_category_bio">Bio</string>
<string name="user_info_sheet_bio_empty">This user hasn\'t set a bio yet.</string> <string name="user_info_sheet_bio_empty">This user hasn\'t set a bio yet.</string>
<string name="user_info_sheet_bio_not_found">This user\'s bio could not be fetched. Please verify you share a server or are friends.</string> <string name="user_info_sheet_bio_not_found">This user\'s bio could not be fetched. Please verify you share a server or are friends.</string>