From 001e58bc3cebefa785a21ffef2d2735e916b4b47 Mon Sep 17 00:00:00 2001 From: Infi Date: Thu, 3 Oct 2024 19:31:56 +0200 Subject: [PATCH] feat: youtube embed support Signed-off-by: Infi --- .../java/chat/revolt/api/schemas/Messages.kt | 10 +- .../java/chat/revolt/components/chat/Embed.kt | 14 +- .../components/chat/MessageAttachment.kt | 39 +++-- .../chat/specialembeds/SpecialEmbedSwitch.kt | 13 ++ .../chat/specialembeds/YouTubeEmbed.kt | 147 ++++++++++++++++++ app/src/main/res/values/strings.xml | 2 + 6 files changed, 202 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/chat/revolt/components/chat/specialembeds/SpecialEmbedSwitch.kt create mode 100644 app/src/main/java/chat/revolt/components/chat/specialembeds/YouTubeEmbed.kt 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 d40e1f60..f00765d6 100644 --- a/app/src/main/java/chat/revolt/api/schemas/Messages.kt +++ b/app/src/main/java/chat/revolt/api/schemas/Messages.kt @@ -82,7 +82,15 @@ data class Image( @Serializable data class Special( - val type: String? = null + val type: String? = null, + val id: String? = null, + val timestamp: String? = null, + @SerialName("content_type") + val contentType: String? = null, + @SerialName("album_id") + val albumID: String? = null, + @SerialName("track_id") + val trackID: String? = null, ) @Serializable diff --git a/app/src/main/java/chat/revolt/components/chat/Embed.kt b/app/src/main/java/chat/revolt/components/chat/Embed.kt index 0343b561..c154b6b5 100644 --- a/app/src/main/java/chat/revolt/components/chat/Embed.kt +++ b/app/src/main/java/chat/revolt/components/chat/Embed.kt @@ -9,10 +9,10 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.surfaceColorAtElevation @@ -28,6 +28,7 @@ import chat.revolt.api.internals.BrushCompat import chat.revolt.api.internals.solidColor import chat.revolt.api.routes.microservices.january.asJanuaryProxyUrl import chat.revolt.api.schemas.Embed +import chat.revolt.components.chat.specialembeds.SpecialEmbedSwitch import chat.revolt.components.generic.RemoteImage import chat.revolt.components.markdown.RichMarkdown import chat.revolt.api.schemas.Embed as EmbedSchema @@ -42,7 +43,7 @@ fun RegularEmbed( modifier = modifier .clip(MaterialTheme.shapes.medium) .background(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)) - .fillMaxWidth() + .wrapContentWidth(Alignment.Start) .height(IntrinsicSize.Min) ) { // Stripe at the left side of the embed @@ -58,7 +59,6 @@ fun RegularEmbed( Box( modifier = Modifier - .fillMaxWidth() .padding(8.dp) ) { Column { @@ -74,8 +74,7 @@ fun RegularEmbed( } else { Modifier } - ) - .fillMaxWidth(), + ), verticalAlignment = Alignment.CenterVertically ) { embed.iconURL?.let { @@ -133,6 +132,11 @@ fun RegularEmbed( description = null // decorative ) } + + // Special + embed.special?.let { + SpecialEmbedSwitch(it) + } } } } diff --git a/app/src/main/java/chat/revolt/components/chat/MessageAttachment.kt b/app/src/main/java/chat/revolt/components/chat/MessageAttachment.kt index e25b4db1..b773f221 100644 --- a/app/src/main/java/chat/revolt/components/chat/MessageAttachment.kt +++ b/app/src/main/java/chat/revolt/components/chat/MessageAttachment.kt @@ -12,10 +12,10 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.material3.LocalContentColor import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.surfaceColorAtElevation @@ -90,6 +90,25 @@ fun ImageAttachment(attachment: AutumnResource) { } } +@Composable +fun VideoPlayButton() { + Box( + modifier = Modifier + .width(48.dp) + .aspectRatio(1f) + .clip(MaterialTheme.shapes.medium) + .background(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)) + ) + + Icon( + imageVector = Icons.Default.PlayArrow, + contentDescription = stringResource(id = R.string.media_viewer_play), + modifier = Modifier + .width(32.dp) + .aspectRatio(1f) + ) +} + @Composable fun VideoAttachment(attachment: AutumnResource) { val url = "$REVOLT_FILES/attachments/${attachment.id}/${attachment.filename}" @@ -114,22 +133,8 @@ fun VideoAttachment(attachment: AutumnResource) { ), description = attachment.filename ?: "Video" ) - - Box( - modifier = Modifier - .width(48.dp) - .aspectRatio(1f) - .clip(MaterialTheme.shapes.medium) - .background(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)) - ) - - Icon( - imageVector = Icons.Default.PlayArrow, - contentDescription = stringResource(id = R.string.media_viewer_play), - modifier = Modifier - .width(32.dp) - .aspectRatio(1f) - ) + + VideoPlayButton() } } } diff --git a/app/src/main/java/chat/revolt/components/chat/specialembeds/SpecialEmbedSwitch.kt b/app/src/main/java/chat/revolt/components/chat/specialembeds/SpecialEmbedSwitch.kt new file mode 100644 index 00000000..06e08e52 --- /dev/null +++ b/app/src/main/java/chat/revolt/components/chat/specialembeds/SpecialEmbedSwitch.kt @@ -0,0 +1,13 @@ +package chat.revolt.components.chat.specialembeds + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import chat.revolt.api.schemas.Special + +@Composable +fun SpecialEmbedSwitch(special: Special, modifier: Modifier = Modifier) { + when (special.type) { + "YouTube" -> YoutubeEmbedSwitch(special, modifier) + else -> {} + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/components/chat/specialembeds/YouTubeEmbed.kt b/app/src/main/java/chat/revolt/components/chat/specialembeds/YouTubeEmbed.kt new file mode 100644 index 00000000..878c0df2 --- /dev/null +++ b/app/src/main/java/chat/revolt/components/chat/specialembeds/YouTubeEmbed.kt @@ -0,0 +1,147 @@ +package chat.revolt.components.chat.specialembeds + +import android.annotation.SuppressLint +import android.webkit.WebView +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.viewinterop.AndroidView +import chat.revolt.R +import chat.revolt.api.schemas.Special +import chat.revolt.components.chat.VideoPlayButton +import chat.revolt.components.generic.RemoteImage +import org.intellij.lang.annotations.Language + +@Language("HTML") +private const val YOUTUBE_EMBED_TEMPLATE = """ + + + + + + + + + + +""" + +@SuppressLint("SetJavaScriptEnabled") +@Composable +fun YouTubeEmbed(special: Special, modifier: Modifier = Modifier) { + if (special.id == null) return + + AndroidView( + factory = { ctx -> + WebView(ctx).apply { + settings.apply { + javaScriptEnabled = true + mediaPlaybackRequiresUserGesture = false + javaScriptCanOpenWindowsAutomatically = true + builtInZoomControls = false + loadWithOverviewMode = true + useWideViewPort = true + displayZoomControls = false + setSupportZoom(false) + isVerticalScrollBarEnabled = false + isHorizontalScrollBarEnabled = false + } + } + }, + update = { + it.loadDataWithBaseURL( + null, + YOUTUBE_EMBED_TEMPLATE + .replace("{{videoId}}", special.id) + .replace("{{useTimestamp}}", (special.timestamp != null).toString()) + .replace("{{timestamp}}", special.timestamp ?: ""), + "text/html", + "UTF-8", + null + ) + }, + modifier = modifier + .clip(MaterialTheme.shapes.medium) + .width(with(LocalDensity.current) { 1280.toDp() }) + .aspectRatio(16f / 9f) + ) +} + +/** + * A switch that displays the thumbnail of a YouTube video and switches to the embedded player when clicked. + * This ensures that the video is only loaded when the user wants to watch it for bandwidth reasons. + */ +@Composable +fun YoutubeEmbedSwitch(special: Special, modifier: Modifier = Modifier) { + var embedEnabled by remember { mutableStateOf(false) } + + Crossfade(targetState = embedEnabled, label = "embed enabled") { + if (it) { + YouTubeEmbed(special, modifier) + } else { + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .clip(MaterialTheme.shapes.medium) + .clickable { + embedEnabled = true + } + .width(with(LocalDensity.current) { 1280.toDp() }) + .aspectRatio(16f / 9f) + .wrapContentHeight() + .background(MaterialTheme.colorScheme.surface) + ) { + RemoteImage( + "https://i3.ytimg.com/vi/${special.id ?: "none"}/maxresdefault.jpg", + stringResource(R.string.message_embed_special_youtube_switch_alt), + width = 1280, + height = 720, + ) + + VideoPlayButton() + } + } + } +} \ 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 cdd00fc5..cce70f1e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -197,6 +197,8 @@ Blocked message Failed to send, long press for options + Tap to play video from YouTube + Ownership changed Channel icon changed Channel description changed