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(