From 97d5f367d10e9b4990fdc82880781fff63f8c9f5 Mon Sep 17 00:00:00 2001 From: Infi Date: Sun, 3 Sep 2023 01:12:49 +0300 Subject: [PATCH] feat: placeholder web based markdown renderer Signed-off-by: Infi --- app/build.gradle | 1 + .../revolt/components/generic/WebMarkdown.kt | 260 ++++++++++++++++++ .../chat/revolt/sheets/UserContextSheet.kt | 7 +- 3 files changed, 265 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/chat/revolt/components/generic/WebMarkdown.kt diff --git a/app/build.gradle b/app/build.gradle index 75485ae6..e42129e9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -174,6 +174,7 @@ dependencies { // Other AndroidX libraries - used for various things and never seem to have a consistent version implementation 'androidx.documentfile:documentfile:1.0.1' implementation "androidx.browser:browser:1.6.0" + implementation "androidx.webkit:webkit:1.7.0" implementation "androidx.datastore:datastore-preferences:1.1.0-alpha04" implementation "androidx.datastore:datastore:1.1.0-alpha04" diff --git a/app/src/main/java/chat/revolt/components/generic/WebMarkdown.kt b/app/src/main/java/chat/revolt/components/generic/WebMarkdown.kt new file mode 100644 index 00000000..81efc463 --- /dev/null +++ b/app/src/main/java/chat/revolt/components/generic/WebMarkdown.kt @@ -0,0 +1,260 @@ +package chat.revolt.components.generic + +import android.annotation.SuppressLint +import android.content.Intent +import android.net.Uri +import android.view.ViewGroup.LayoutParams +import android.webkit.JavascriptInterface +import android.webkit.WebChromeClient +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.FrameLayout +import androidx.browser.customtabs.CustomTabColorSchemeParams +import androidx.browser.customtabs.CustomTabsIntent +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.LocalContentColor +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.graphics.toArgb +import androidx.compose.ui.viewinterop.AndroidView +import androidx.webkit.WebViewAssetLoader +import chat.revolt.activities.InviteActivity +import chat.revolt.api.REVOLT_APP +import org.intellij.lang.annotations.Language + +// TODO: Obvious placeholder. +@Language("HTML") +private const val HTML_TEMPLATE = """ + + + + + + + + + +
%s
+ + + + + +""" + +private fun argbAsCssColour(argb: Int): String { + val alpha = (argb shr 24 and 0xff) / 255.0f + val red = argb shr 16 and 0xff + val green = argb shr 8 and 0xff + val blue = argb and 0xff + return String.format("#%02x%02x%02x%02x", red, green, blue, (alpha * 255).toInt()) +} + +/** + * WebView-backed Markdown renderer that supports all Markdown features + * including KaTeX + */ +@SuppressLint("SetJavaScriptEnabled") +@Composable +fun WebMarkdown( + text: String, + maskLoading: Boolean = false, + modifier: Modifier = Modifier, +) { + val contentColour = LocalContentColor.current + val materialColourScheme = MaterialTheme.colorScheme + + var finishedLoading by remember { mutableStateOf(false) } + + if (!finishedLoading && maskLoading) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator() + } + } + + AndroidView( + modifier = modifier, + factory = { context -> + WebView(context).apply { + val cssContentColour = argbAsCssColour(contentColour.toArgb()) + val cssPrimaryColour = argbAsCssColour(materialColourScheme.primary.toArgb()) + + val html = String.format( + HTML_TEMPLATE, + cssContentColour, + cssPrimaryColour, + text.replace("&", "&").replace("<", "<").replace(">", ">") + ) + + val assetLoader = WebViewAssetLoader.Builder() + .setDomain(Uri.parse(REVOLT_APP).host!!) + .addPathHandler( + "/_android_assets/", + WebViewAssetLoader.AssetsPathHandler(context) + ) + .addPathHandler( + "/_android_res/", + WebViewAssetLoader.ResourcesPathHandler(context) + ) + .build() + + webChromeClient = object : WebChromeClient() {} + webViewClient = object : WebViewClient() { + override fun shouldInterceptRequest( + view: WebView?, + request: WebResourceRequest? + ): WebResourceResponse? { + return request?.let { assetLoader.shouldInterceptRequest(it.url) } + } + + override fun shouldOverrideUrlLoading( + view: WebView?, + webResourceRequest: WebResourceRequest + ): Boolean { + // Capture clicks on invite links + if (webResourceRequest.url.host == "rvlt.gg" || + (webResourceRequest.url.host?.endsWith("revolt.chat") == true && webResourceRequest.url.path?.startsWith( + "/invite" + ) == true) + ) { + val intent = Intent( + context, + InviteActivity::class.java + ).setAction(Intent.ACTION_VIEW) + + intent.data = webResourceRequest.url + context.startActivity(intent) + + return true + } + + // Otherwise, open the link in the browser using androidx.browser + val customTab = CustomTabsIntent.Builder() + .setShowTitle(true) + .setDefaultColorSchemeParams( + CustomTabColorSchemeParams.Builder() + .setToolbarColor(materialColourScheme.background.toArgb()) + .build() + ) + .build() + customTab.launchUrl(context, webResourceRequest.url) + + // Prevent the WebView from navigating to the URL + return true + } + } + + loadDataWithBaseURL( + REVOLT_APP, + html, + "text/html; charset=utf-8", + "UTF-8", + null + ) + + settings.apply { + javaScriptEnabled = true + setSupportZoom(false) + setSupportMultipleWindows(false) + isVerticalScrollBarEnabled = false + isHorizontalScrollBarEnabled = false + cacheMode = WebSettings.LOAD_NO_CACHE + } + + addJavascriptInterface( + object { + @JavascriptInterface + fun onLoaded() { + finishedLoading = true + } + }, + "Android" + ) + + setBackgroundColor(android.graphics.Color.TRANSPARENT) + layoutParams = FrameLayout.LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT + ) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/sheets/UserContextSheet.kt b/app/src/main/java/chat/revolt/sheets/UserContextSheet.kt index 83be32f5..236a804b 100644 --- a/app/src/main/java/chat/revolt/sheets/UserContextSheet.kt +++ b/app/src/main/java/chat/revolt/sheets/UserContextSheet.kt @@ -31,7 +31,7 @@ import chat.revolt.api.internals.solidColor import chat.revolt.api.routes.user.fetchUserProfile import chat.revolt.api.schemas.Profile import chat.revolt.components.chat.RoleChip -import chat.revolt.components.generic.UIMarkdown +import chat.revolt.components.generic.WebMarkdown import chat.revolt.components.screens.settings.RawUserOverview @OptIn(ExperimentalLayoutApi::class) @@ -107,9 +107,10 @@ fun UserContextSheet( modifier = Modifier.padding(vertical = 10.dp) ) - if (profile?.content != null) { - UIMarkdown( + if (profile?.content.isNullOrBlank().not()) { + WebMarkdown( text = profile!!.content!!, + maskLoading = true ) } else if (profile != null) { Text(