diff --git a/app/src/main/java/chat/revolt/api/schemas/Messages.kt b/app/src/main/java/chat/revolt/api/schemas/Messages.kt index f00765d6..8eb7ddbe 100644 --- a/app/src/main/java/chat/revolt/api/schemas/Messages.kt +++ b/app/src/main/java/chat/revolt/api/schemas/Messages.kt @@ -20,6 +20,7 @@ data class Message( val mentions: List? = null, val masquerade: Masquerade? = null, val system: SystemInfo? = null, + val webhook: WebHook? = null, 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 ) { @@ -110,3 +111,9 @@ data class SystemInfo( val to: String? = null, val content: String? = null ) + +@Serializable +data class WebHook( + val avatar: String? = null, + val name: String? = null, +) \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/callbacks/ActionChannel.kt b/app/src/main/java/chat/revolt/callbacks/ActionChannel.kt index 982321d5..c8f0d630 100644 --- a/app/src/main/java/chat/revolt/callbacks/ActionChannel.kt +++ b/app/src/main/java/chat/revolt/callbacks/ActionChannel.kt @@ -14,6 +14,7 @@ sealed class Action { data class ReportUser(val userId: String) : Action() data class ReportMessage(val messageId: String) : Action() data class OpenVoiceChannelOverlay(val channelId: String) : Action() + data object OpenWebhookSheet : Action() } val ActionChannel = Channel( diff --git a/app/src/main/java/chat/revolt/components/chat/InlineBadge.kt b/app/src/main/java/chat/revolt/components/chat/InlineBadge.kt index 1d969180..2aeb39e6 100644 --- a/app/src/main/java/chat/revolt/components/chat/InlineBadge.kt +++ b/app/src/main/java/chat/revolt/components/chat/InlineBadge.kt @@ -13,7 +13,8 @@ enum class InlineBadge { Bot, Bridge, PlatformModeration, - TeamMember + TeamMember, + Webhook } @Composable @@ -50,6 +51,13 @@ fun InlineBadge( tint = colour, 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, platformModeration: Boolean = false, teamMember: Boolean = false, + webhook: Boolean = false, colour: Color = Color.Unspecified, precedingIfAny: @Composable () -> Unit = {}, followingIfAny: @Composable () -> Unit = {} @@ -99,6 +108,13 @@ fun InlineBadges( colour = colour ) } + if (webhook) { + InlineBadge( + badge = InlineBadge.Webhook, + modifier = modifier, + colour = colour + ) + } } if (hasBadges) { diff --git a/app/src/main/java/chat/revolt/components/chat/Message.kt b/app/src/main/java/chat/revolt/components/chat/Message.kt index 45dc3900..1e77a9dd 100644 --- a/app/src/main/java/chat/revolt/components/chat/Message.kt +++ b/app/src/main/java/chat/revolt/components/chat/Message.kt @@ -180,6 +180,8 @@ fun Message( canReply: Boolean = false, onReply: () -> Unit = {}, onAddReaction: () -> Unit = {}, + fromWebhook: Boolean = false, + webhookName: String? = null ) { val author = RevoltAPI.userCache[message.author] ?: return CircularProgressIndicator() val context = LocalContext.current @@ -304,7 +306,7 @@ fun Message( if (message.tail == false) { Row(verticalAlignment = Alignment.CenterVertically) { Text( - text = authorName(message), + text = webhookName ?: authorName(message), style = LocalTextStyle.current.copy( fontWeight = FontWeight.Bold, brush = authorColour(message) @@ -328,6 +330,7 @@ fun Message( bridge = message.masquerade != null && author.bot != null, platformModeration = author.id == SpecialUsers.PLATFORM_MODERATION_USER, teamMember = author.id in SpecialUsers.TEAM_MEMBER_FLAIRS.keys, + webhook = fromWebhook, colour = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f), modifier = Modifier.size(16.dp), precedingIfAny = { 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 fefb4585..1a57cad7 100644 --- a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt @@ -78,6 +78,7 @@ import chat.revolt.sheets.ReactionInfoSheet import chat.revolt.sheets.ServerContextSheet import chat.revolt.sheets.StatusSheet import chat.revolt.sheets.UserInfoSheet +import chat.revolt.sheets.WebHookUserSheet import com.airbnb.lottie.RenderMode import com.airbnb.lottie.compose.LottieAnimation import com.airbnb.lottie.compose.LottieCompositionSpec @@ -229,6 +230,8 @@ fun ChatRouterScreen( var userContextSheetTarget by remember { mutableStateOf("") } var userContextSheetServer by remember { mutableStateOf(null) } + var showWebhookInfoSheet by remember { mutableStateOf(false) } + var showChannelUnavailableAlert by remember { mutableStateOf(false) } var showLinkInfoSheet by remember { mutableStateOf(false) } @@ -389,6 +392,10 @@ fun ChatRouterScreen( voiceChannelOverlayChannelId = action.channelId 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) { ReportUserDialog( onDismiss = { showReportUser = false }, 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 8b236826..4c71506c 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 @@ -134,7 +134,6 @@ import kotlinx.coroutines.launch import kotlinx.datetime.Instant import java.io.File import kotlin.math.max -import kotlin.math.min sealed class ChannelScreenItem { data class RegularMessage(val message: Message) : ChannelScreenItem() @@ -603,14 +602,20 @@ fun ChannelScreen( } }, onAvatarClick = { - item.message.author?.let { author -> + if (item.message.webhook != null) { scope.launch { - ActionChannel.send( - Action.OpenUserSheet( - author, - viewModel.channel?.server + ActionChannel.send(Action.OpenWebhookSheet) + } + } else { + item.message.author?.let { author -> + scope.launch { + ActionChannel.send( + Action.OpenUserSheet( + author, + viewModel.channel?.server + ) ) - ) + } } } }, @@ -631,7 +636,9 @@ fun ChannelScreen( reactSheetTarget = messageId reactSheetShown = true } - } + }, + fromWebhook = item.message.webhook != null, + webhookName = item.message.webhook?.name ) } diff --git a/app/src/main/java/chat/revolt/sheets/WebHookUserSheet.kt b/app/src/main/java/chat/revolt/sheets/WebHookUserSheet.kt new file mode 100644 index 00000000..50d6659f --- /dev/null +++ b/app/src/main/java/chat/revolt/sheets/WebHookUserSheet.kt @@ -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)) +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_hook_24dp.xml b/app/src/main/res/drawable/ic_hook_24dp.xml new file mode 100644 index 00000000..15fd397e --- /dev/null +++ b/app/src/main/res/drawable/ic_hook_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 81030b6d..a91a4336 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -125,6 +125,7 @@ From linked channel Platform moderation Revolt team member + Webhook Unknown @@ -352,6 +353,10 @@ Open user info Can\'t resolve this user This user may have been deleted or you may not have permission to view them. + Is it a bird, is it a plane? + No, it\'s a webhook! + 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. + Learn more Bio This user hasn\'t set a bio yet. This user\'s bio could not be fetched. Please verify you share a server or are friends.