From 8d6a76ccb5da1d3dbaa672a38c13e2227fce128c Mon Sep 17 00:00:00 2001 From: Sweetbread Date: Fri, 20 Feb 2026 17:28:12 +0300 Subject: [PATCH] wip --- .../java/ru/risdeveau/pixeldragon/Common.kt | 1 + .../java/ru/risdeveau/pixeldragon/api/sync.kt | 207 +++--- .../pixeldragon/ui/activity/MainActivity.kt | 33 +- .../risdeveau/pixeldragon/ui/layout/Room.kt | 682 +++++++++--------- 4 files changed, 460 insertions(+), 463 deletions(-) diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/Common.kt b/app/src/main/java/ru/risdeveau/pixeldragon/Common.kt index 57642c7..97990e1 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/Common.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/Common.kt @@ -34,6 +34,7 @@ lateinit var token: String object AccountData : Preferences("system_parameters") { var token by stringOrNullPref("token", null) + var userId by stringOrNullPref("user_id", null) var homeserver by stringOrNullPref("homeserver", null) var syncLastBatch by stringOrNullPref("next_batch", null) var filter by stringOrNullPref("filter", null) diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/api/sync.kt b/app/src/main/java/ru/risdeveau/pixeldragon/api/sync.kt index d3e71d2..73af08b 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/api/sync.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/api/sync.kt @@ -15,6 +15,7 @@ import io.ktor.client.request.setBody import io.ktor.client.statement.bodyAsText import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode +import io.ktor.http.contentLength import io.ktor.http.contentType import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -26,6 +27,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import org.json.JSONObject +import ru.risdeveau.pixeldragon.AccountData import ru.risdeveau.pixeldragon.AccountData.syncLastBatch import ru.risdeveau.pixeldragon.baseUrl import ru.risdeveau.pixeldragon.client @@ -48,18 +50,28 @@ class MatrixSyncService { // } fun startSync() { + if (AccountData.token == null) + Log.wtf("MatrixSyncService", "Token is null") + if (AccountData.userId == null) + Log.wtf("MatrixSyncService", "User ID is null") + if (AccountData.homeserver == null) + Log.wtf("MatrixSyncService", "Homeserver is null") + + Log.i("MatrixSyncService", "Start syncing") // if (!isInitialized) // throw IllegalStateException("Sync service not initialized") if (syncJob?.isActive == true) return syncJob = CoroutineScope(Dispatchers.IO).launch { - if (syncLastBatch == "") { + if (syncLastBatch == null) { + Log.i("MatrixSyncService", "Init syncing") _syncState.value = SyncState.Syncing try { - processSyncResponse(initialSync()) + initialSync() _syncState.value = SyncState.Idle } catch (e: Exception) { + Log.e("MatrixSyncService", "Initial sync error", e) _syncState.value = SyncState.Error(e.message) } } @@ -101,44 +113,44 @@ class MatrixSyncService { fun isSyncActive(): Boolean = syncJob?.isActive == true - private suspend fun processSyncResponse(response: JSONObject) { -// Log.d("syncResponse", response.toString(2)) -// val newMessages = mutableListOf() -// val roomUpdates = mutableListOf() -// -// response.rooms?.join?.forEach { (roomId, roomData) -> -// // Process timeline events (messages) -// roomData.timeline?.events?.forEach { event -> -// val message = event.toMessage(roomId) -// database.messageDao().insertMessage(message) -// newMessages.add(message) -// } -// -// // Process room state updates -// roomData.state?.events?.forEach { event -> -// when (event.type) { -// "m.room.name", "m.room.avatar" -> { -// roomUpdates.add(RoomUpdate(roomId, event.type, event.content)) -// } -// } -// } -// -// // Process ephemeral events (typing, receipts) -// roomData.ephemeral?.events?.forEach { event -> -// when (event.type) { -// "m.typing" -> handleTypingEvent(roomId, event) -// "m.receipt" -> handleReceiptEvent(roomId, event) -// } -// } -// } -// -// // Notify the app about new data -// if (newMessages.isNotEmpty()) { -// _newMessages.emit(newMessages) -// } -// if (roomUpdates.isNotEmpty()) { -// _roomUpdates.emit(roomUpdates) -// } + private suspend fun parseSyncResponse(response: JSONObject) { + Log.v("syncResponse", response.toString(2)) + val newMessages = mutableListOf() + val roomUpdates = mutableListOf() + + response.rooms?.join?.forEach { (roomId, roomData) -> + // Process timeline events (messages) + roomData.timeline?.events?.forEach { event -> + val message = event.toMessage(roomId) + database.messageDao().insertMessage(message) + newMessages.add(message) + } + + // Process room state updates + roomData.state?.events?.forEach { event -> + when (event.type) { + "m.room.name", "m.room.avatar" -> { + roomUpdates.add(RoomUpdate(roomId, event.type, event.content)) + } + } + } + + // Process ephemeral events (typing, receipts) + roomData.ephemeral?.events?.forEach { event -> + when (event.type) { + "m.typing" -> handleTypingEvent(roomId, event) + "m.receipt" -> handleReceiptEvent(roomId, event) + } + } + } + + // Notify the app about new data + if (newMessages.isNotEmpty()) { + _newMessages.emit(newMessages) + } + if (roomUpdates.isNotEmpty()) { + _roomUpdates.emit(roomUpdates) + } } } @@ -156,70 +168,42 @@ suspend fun sync(): JSONObject { } suspend fun initialSync(): JSONObject { - val initialFilter = """ + fetchRoomMeta() +} + +/** + * Fetch rooms metadata during initial sync + */ +suspend fun fetchRoomMeta() { + val filterId = getFilterId(""" { - "room": { - "state": { - "types": [ - "m.room.name", - "m.room.avatar", - "m.room.canonical_alias", - "m.room.encryption", - "m.room.tombstone", - "m.room.power_levels", - "m.room.member" - ], - "lazy_load_members": true, - "not_types": [] + "room": { + "state": { + "types": [ + "m.room.name", + "m.room.avatar", + "m.room.canonical_alias", + "m.room.member" + ], + "lazy_load_members": true + }, + "timeline": { + "limit": 0, + "types": [] + }, + "ephemeral": { + "types": [] + }, + "include_leave": false }, - "timeline": { - "limit": 10, - "types": ["m.room.message"], - "not_types": [ - "m.room.name", - "m.room.avatar", - "m.room.canonical_alias", - "m.room.encryption", - "m.room.tombstone", - "m.room.power_levels", - "m.room.member", - "m.call.*" - ] - }, - "ephemeral": { - "types": [], - "not_types": ["m.typing", "m.receipt"] - }, - "include_leave": false - }, - "presence": { - "types": [], - "not_types": ["*"] - }, - "event_format": "client", - "event_fields": [ - "type", - "content", - "sender", - "state_key", - "room_id", - "origin_server_ts" - ] + "presence": { + "types": [] + } } - """.trimIndent() - - var r = client.post("$baseUrl/user/${ME!!.userId}/filter") { - setBody(initialFilter) - contentType(ContentType.Application.Json) - bearerAuth(token) - } - - if (r.status != HttpStatusCode.OK) - throw IllegalStateException("Failed to create a filter") - val filterId = JSONObject(r.bodyAsText()).getString("filter_id") + """.trimIndent()) // val filterId = "vmNk" - /*val*/ r = client.get("$baseUrl/sync") { + val r = client.get("$baseUrl/sync") { bearerAuth(token) url { parameter("filter", filterId) @@ -228,10 +212,31 @@ suspend fun initialSync(): JSONObject { if (r.status != HttpStatusCode.OK) throw IllegalStateException("Failed to sync") - Log.d("initialSync", "Response size: ${r.bodyAsText().length}") + Log.v("initialSync", "Response size: ${r.contentLength()}") + r.contentLength()?.let { + if (it >= 50*1024*1024) + Log.w("initialSync", "Response size is too large") + } val json = JSONObject(r.bodyAsText()) syncLastBatch = json.getString("next_batch") +} - return json +/** + * Send a filter to the server and get its id + * @param filter JSON Filter + * @return Filter ID + */ +suspend fun getFilterId(filter: String): String { + val userId = AccountData.userId + if (userId == null) + Log.wtf("getFilter", "user_id is not defined") + + val r = client.post("$baseUrl/user/$userId/filter") { + setBody(filter) + contentType(ContentType.Application.Json) + bearerAuth(token) + } + + return JSONObject(r.bodyAsText()).getString("filter_id") } \ No newline at end of file diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/MainActivity.kt b/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/MainActivity.kt index 8aa0952..75441ca 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/MainActivity.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/MainActivity.kt @@ -26,22 +26,29 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import ru.risdeveau.pixeldragon.AccountData import ru.risdeveau.pixeldragon.api.Me import ru.risdeveau.pixeldragon.api.getMe import ru.risdeveau.pixeldragon.initCheck import ru.risdeveau.pixeldragon.syncService -import ru.risdeveau.pixeldragon.ui.layout.Room +//import ru.risdeveau.pixeldragon.ui.layout.Room import ru.risdeveau.pixeldragon.ui.layout.RoomList import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme import splitties.activities.start -var ME: Me? = null class MainActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + if (AccountData.token == null) { + start() + finish() + } else { + syncService.startSync() + } + enableEdgeToEdge() setContent { PixelDragonTheme { @@ -61,22 +68,6 @@ class MainActivity : ComponentActivity() { ) { innerPadding -> val navController = rememberNavController() - LaunchedEffect(Unit) { - if (initCheck()) { - ME = withContext(Dispatchers.IO) { getMe() } - if (ME != null) { - syncService.startSync() - navController.navigate("rooms") - } else { - start() - finish() - } - } else { - start() - finish() - } - } - NavHost(navController = navController, startDestination = "none") { composable("none") { } composable("rooms") { RoomList(Modifier.padding(innerPadding), navController) } @@ -84,9 +75,9 @@ class MainActivity : ComponentActivity() { "room/{rid}", arguments = listOf(navArgument("rid") { type = NavType.StringType }) ) { navBackStackEntry -> - Room(Modifier - .padding(innerPadding) - .fillMaxSize(), navBackStackEntry.arguments!!.getString("rid")!!) +// Room(Modifier +// .padding(innerPadding) +// .fillMaxSize(), navBackStackEntry.arguments!!.getString("rid")!!) } composable( "space/{rid}", diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/Room.kt b/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/Room.kt index a5259be..dba9067 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/Room.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/Room.kt @@ -1,342 +1,342 @@ -/* - * Created by sweetbread - * Copyright (c) 2025. All rights reserved. - */ - -package ru.risdeveau.pixeldragon.ui.layout - -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.webkit.WebResourceRequest -import android.webkit.WebView -import android.webkit.WebViewClient -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.jsoup.Jsoup -import org.jsoup.safety.Safelist -import ru.risdeveau.pixeldragon.api.Event -import ru.risdeveau.pixeldragon.api.getAccountData -import ru.risdeveau.pixeldragon.api.getEventsAround -import ru.risdeveau.pixeldragon.ui.activity.ME -import ru.risdeveau.pixeldragon.ui.item.MXCImage - -@Composable -fun Room(modifier: Modifier = Modifier, rid: String) { - var eventsId by remember { mutableStateOf(listOf()) } - val listState = rememberLazyListState() - - LaunchedEffect(Unit) { - withContext(Dispatchers.IO) { - val readMark = getAccountData(ME!!.userId, rid, "m.fully_read") - val eventsAround = getEventsAround(rid, readMark!!.getString("event_id")) //FIXME: Null check - eventsId = eventsAround.let { - it.before + listOf(it.base) + it.after - } - } - } - - LazyColumn(modifier = modifier, state = listState, reverseLayout = true) { - items(eventsId.reversed()) { event -> - EventItem(event) - } - - item { - if (eventsId.isEmpty()) { - Text("Empty room") - } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun EventItem(event: Event) { - Box (Modifier.fillMaxWidth()) { - when (event.type) { - "m.room.message" -> Column( - Modifier - .fillMaxSize() - .then( - if (event.sender != ME!!.userId) - Modifier.padding(end = 16.dp) - else - Modifier.padding(start = 16.dp) - ) - .padding(4.dp) - .background( - if (event.sender != ME?.userId) - MaterialTheme.colorScheme.surfaceContainer - else - MaterialTheme.colorScheme.primaryContainer - ) - .clip(RoundedCornerShape(16.dp)) - .padding(4.dp) - ) { - Text(event.sender, maxLines = 1, fontWeight = FontWeight.Bold) - - when (val msgtype = event.content.optString("msgtype", null)) { - "m.text" -> when (event.content.optString("format")) { - "org.matrix.custom.html" -> { - if (event.content.getString("body") == event.content.getString("formatted_body")) - Text(event.content.getString("body")) - HtmlRenderer(event.content.getString("formatted_body")) - } - - else -> Text(event.content.getString("body")) - } - - "m.image" -> - MXCImage(event.content.getString("url"), Modifier.fillMaxWidth(.9f)) - - null -> Text(event.content.toString(2)) - - else -> Text("Unknown type: $msgtype", color = MaterialTheme.colorScheme.error) - } - - } - - else -> Text(event.type, - Modifier - .fillMaxHeight() - .padding(4.dp) - .background(MaterialTheme.colorScheme.errorContainer) - .padding(4.dp) - ) - } - } -} - -private fun String.sanitizeHTML(): String { - val matrixSafelist = Safelist() - .addTags( - "h1", "h2", "h3", "h4", "h5", "h6", - "b", "i", "u", "strong", "s", "del", - "sup", "sub", "code", - "table", "thead", "tbody", - "tr", "th", "td", "ul", "ol", "li", - "blockquote", "details", "summary", - "em", "code", "div", "pre", "span", "img" - ) -// .addAttributes("span", -// "data-mx-bg-color", "data-mx-color", -// "data-mx-spoiler", "data-mx-maths" +///* +// * Created by sweetbread +// * Copyright (c) 2025. All rights reserved. +// */ +// +//package ru.risdeveau.pixeldragon.ui.layout +// +//import android.content.Context +//import android.content.Intent +//import android.net.Uri +//import android.webkit.WebResourceRequest +//import android.webkit.WebView +//import android.webkit.WebViewClient +//import androidx.compose.foundation.background +//import androidx.compose.foundation.layout.Box +//import androidx.compose.foundation.layout.Column +//import androidx.compose.foundation.layout.fillMaxHeight +//import androidx.compose.foundation.layout.fillMaxSize +//import androidx.compose.foundation.layout.fillMaxWidth +//import androidx.compose.foundation.layout.padding +//import androidx.compose.foundation.lazy.LazyColumn +//import androidx.compose.foundation.lazy.items +//import androidx.compose.foundation.lazy.rememberLazyListState +//import androidx.compose.foundation.shape.RoundedCornerShape +//import androidx.compose.material3.ExperimentalMaterial3Api +//import androidx.compose.material3.MaterialTheme +//import androidx.compose.material3.Text +//import androidx.compose.runtime.Composable +//import androidx.compose.runtime.DisposableEffect +//import androidx.compose.runtime.LaunchedEffect +//import androidx.compose.runtime.getValue +//import androidx.compose.runtime.mutableStateOf +//import androidx.compose.runtime.remember +//import androidx.compose.runtime.setValue +//import androidx.compose.ui.Modifier +//import androidx.compose.ui.draw.clip +//import androidx.compose.ui.graphics.Color +//import androidx.compose.ui.graphics.toArgb +//import androidx.compose.ui.platform.LocalContext +//import androidx.compose.ui.text.font.FontWeight +//import androidx.compose.ui.unit.dp +//import androidx.compose.ui.viewinterop.AndroidView +//import kotlinx.coroutines.Dispatchers +//import kotlinx.coroutines.withContext +//import org.jsoup.Jsoup +//import org.jsoup.safety.Safelist +//import ru.risdeveau.pixeldragon.api.Event +//import ru.risdeveau.pixeldragon.api.getAccountData +//import ru.risdeveau.pixeldragon.api.getEventsAround +//import ru.risdeveau.pixeldragon.ui.activity.ME +//import ru.risdeveau.pixeldragon.ui.item.MXCImage +// +//@Composable +//fun Room(modifier: Modifier = Modifier, rid: String) { +// var eventsId by remember { mutableStateOf(listOf()) } +// val listState = rememberLazyListState() +// +// LaunchedEffect(Unit) { +// withContext(Dispatchers.IO) { +// val readMark = getAccountData(ME!!.userId, rid, "m.fully_read") +// val eventsAround = getEventsAround(rid, readMark!!.getString("event_id")) //FIXME: Null check +// eventsId = eventsAround.let { +// it.before + listOf(it.base) + it.after +// } +// } +// } +// +// LazyColumn(modifier = modifier, state = listState, reverseLayout = true) { +// items(eventsId.reversed()) { event -> +// EventItem(event) +// } +// +// item { +// if (eventsId.isEmpty()) { +// Text("Empty room") +// } +// } +// } +//} +// +//@OptIn(ExperimentalMaterial3Api::class) +//@Composable +//fun EventItem(event: Event) { +// Box (Modifier.fillMaxWidth()) { +// when (event.type) { +// "m.room.message" -> Column( +// Modifier +// .fillMaxSize() +// .then( +// if (event.sender != ME!!.userId) +// Modifier.padding(end = 16.dp) +// else +// Modifier.padding(start = 16.dp) +// ) +// .padding(4.dp) +// .background( +// if (event.sender != ME?.userId) +// MaterialTheme.colorScheme.surfaceContainer +// else +// MaterialTheme.colorScheme.primaryContainer +// ) +// .clip(RoundedCornerShape(16.dp)) +// .padding(4.dp) +// ) { +// Text(event.sender, maxLines = 1, fontWeight = FontWeight.Bold) +// +// when (val msgtype = event.content.optString("msgtype", null)) { +// "m.text" -> when (event.content.optString("format")) { +// "org.matrix.custom.html" -> { +// if (event.content.getString("body") == event.content.getString("formatted_body")) +// Text(event.content.getString("body")) +// HtmlRenderer(event.content.getString("formatted_body")) +// } +// +// else -> Text(event.content.getString("body")) +// } +// +// "m.image" -> +// MXCImage(event.content.getString("url"), Modifier.fillMaxWidth(.9f)) +// +// null -> Text(event.content.toString(2)) +// +// else -> Text("Unknown type: $msgtype", color = MaterialTheme.colorScheme.error) +// } +// +// } +// +// else -> Text(event.type, +// Modifier +// .fillMaxHeight() +// .padding(4.dp) +// .background(MaterialTheme.colorScheme.errorContainer) +// .padding(4.dp) +// ) +// } +// } +//} +// +//private fun String.sanitizeHTML(): String { +// val matrixSafelist = Safelist() +// .addTags( +// "h1", "h2", "h3", "h4", "h5", "h6", +// "b", "i", "u", "strong", "s", "del", +// "sup", "sub", "code", +// "table", "thead", "tbody", +// "tr", "th", "td", "ul", "ol", "li", +// "blockquote", "details", "summary", +// "em", "code", "div", "pre", "span", "img" // ) - .addAttributes("a", - "target", "href" - ) - .addAttributes("img", - "width", "height", "alt", "title", "src" - ) - .addAttributes("ol", "start") - .addAttributes("code", "class") - .addAttributes("div", "data-mx-maths") - - val doc = Jsoup.parse(this) - doc.select("mx-reply").remove() - - val out = Jsoup.clean(doc.body().toString(), matrixSafelist) - return out -} - - -@Composable -fun HtmlRenderer( - htmlContent: String, - modifier: Modifier = Modifier -) { - val context = LocalContext.current - val webView = remember { WebView(context).apply { - settings.apply { - javaScriptEnabled = false - loadWithOverviewMode = true - useWideViewPort = true - } - isVerticalScrollBarEnabled = false - setBackgroundColor(Color.Transparent.toArgb()) - } } - - val css = """ - body { - font-family: -apple-system, sans-serif; - font-size: 16px; - line-height: 1.6; - color: ${colorToCss(MaterialTheme.colorScheme.onBackground)}; - margin: 0; - padding: 0; - } - h1, h2, h3, h4, h5, h6 { - font-weight: bold; - padding: 0; - } - h1 { font-size: 24px; } - h2 { font-size: 22px; } - h3 { font-size: 20px; } - h4 { font-size: 18px; } - h5 { font-size: 16px; } - h6 { font-size: 14px; } - a { - color: ${colorToCss(MaterialTheme.colorScheme.primary)}; - text-decoration: none; - } - img { - max-width: 100%; - height: auto; - display: block; - margin: 12px 0; - } - table { - width: 100%; - border-collapse: collapse; - margin: 16px 0; - } - th, td { - border: 1px solid ${colorToCss(MaterialTheme.colorScheme.outline)}; - padding: 12px; - text-align: left; - } - th { - background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)}; - font-weight: bold; - } - blockquote { - border-left: 4px solid ${colorToCss(MaterialTheme.colorScheme.primary)}; - padding-left: 16px; - margin-left: 0; - color: ${colorToCss(MaterialTheme.colorScheme.onSurfaceVariant)}; - } - pre { - background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)}; - padding: 16px; - overflow: auto; - border-radius: 4px; - } - code { - font-family: monospace; - background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)}; - padding: 2px 4px; - border-radius: 4px; - } - hr { - border: 0; - height: 1px; - background-color: ${colorToCss(MaterialTheme.colorScheme.outline)}; - margin: 24px 0; - } - ul, ol { - padding-left: 24px; - margin: 12px 0; - } - li { - margin-bottom: 8px; - } - details { - margin: 12px 0; - } - summary { - font-weight: bold; - cursor: pointer; - } - @media (prefers-color-scheme: dark) { - :root { - --border-color: ${colorToCss(MaterialTheme.colorScheme.outline)}; - } - } - """.trimIndent() - - LaunchedEffect(htmlContent) { - webView.loadDataWithBaseURL( - null, - wrapHtml(htmlContent, css), - "text/html", - "UTF-8", - null - ) - } - - DisposableEffect(webView) { - onDispose { - webView.destroy() - } - } - - AndroidView( - factory = { webView }, - modifier = modifier, - update = { view -> - view.webViewClient = SafeWebViewClient(context) - } - ) -} - -private class SafeWebViewClient( - private val context: Context -) : WebViewClient() { - override fun shouldOverrideUrlLoading( - view: WebView, - request: WebResourceRequest - ): Boolean { - val url = request.url.toString() - try { - // Открываем ссылки во внешнем браузере - context.startActivity( - Intent(Intent.ACTION_VIEW, Uri.parse(url)) - ) - return true - } catch (e: Exception) { - // Обработка ошибок открытия ссылки - return false - } - } -} - -private fun wrapHtml(content: String, css: String): String { - return """ - - - - - - - - ${content.sanitizeHTML()} - - - """.trimIndent() -} - -private fun colorToCss(color: Color): String { - val argb = color.toArgb() - return String.format("#%06X", 0xFFFFFF and argb) -} \ No newline at end of file +//// .addAttributes("span", +//// "data-mx-bg-color", "data-mx-color", +//// "data-mx-spoiler", "data-mx-maths" +//// ) +// .addAttributes("a", +// "target", "href" +// ) +// .addAttributes("img", +// "width", "height", "alt", "title", "src" +// ) +// .addAttributes("ol", "start") +// .addAttributes("code", "class") +// .addAttributes("div", "data-mx-maths") +// +// val doc = Jsoup.parse(this) +// doc.select("mx-reply").remove() +// +// val out = Jsoup.clean(doc.body().toString(), matrixSafelist) +// return out +//} +// +// +//@Composable +//fun HtmlRenderer( +// htmlContent: String, +// modifier: Modifier = Modifier +//) { +// val context = LocalContext.current +// val webView = remember { WebView(context).apply { +// settings.apply { +// javaScriptEnabled = false +// loadWithOverviewMode = true +// useWideViewPort = true +// } +// isVerticalScrollBarEnabled = false +// setBackgroundColor(Color.Transparent.toArgb()) +// } } +// +// val css = """ +// body { +// font-family: -apple-system, sans-serif; +// font-size: 16px; +// line-height: 1.6; +// color: ${colorToCss(MaterialTheme.colorScheme.onBackground)}; +// margin: 0; +// padding: 0; +// } +// h1, h2, h3, h4, h5, h6 { +// font-weight: bold; +// padding: 0; +// } +// h1 { font-size: 24px; } +// h2 { font-size: 22px; } +// h3 { font-size: 20px; } +// h4 { font-size: 18px; } +// h5 { font-size: 16px; } +// h6 { font-size: 14px; } +// a { +// color: ${colorToCss(MaterialTheme.colorScheme.primary)}; +// text-decoration: none; +// } +// img { +// max-width: 100%; +// height: auto; +// display: block; +// margin: 12px 0; +// } +// table { +// width: 100%; +// border-collapse: collapse; +// margin: 16px 0; +// } +// th, td { +// border: 1px solid ${colorToCss(MaterialTheme.colorScheme.outline)}; +// padding: 12px; +// text-align: left; +// } +// th { +// background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)}; +// font-weight: bold; +// } +// blockquote { +// border-left: 4px solid ${colorToCss(MaterialTheme.colorScheme.primary)}; +// padding-left: 16px; +// margin-left: 0; +// color: ${colorToCss(MaterialTheme.colorScheme.onSurfaceVariant)}; +// } +// pre { +// background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)}; +// padding: 16px; +// overflow: auto; +// border-radius: 4px; +// } +// code { +// font-family: monospace; +// background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)}; +// padding: 2px 4px; +// border-radius: 4px; +// } +// hr { +// border: 0; +// height: 1px; +// background-color: ${colorToCss(MaterialTheme.colorScheme.outline)}; +// margin: 24px 0; +// } +// ul, ol { +// padding-left: 24px; +// margin: 12px 0; +// } +// li { +// margin-bottom: 8px; +// } +// details { +// margin: 12px 0; +// } +// summary { +// font-weight: bold; +// cursor: pointer; +// } +// @media (prefers-color-scheme: dark) { +// :root { +// --border-color: ${colorToCss(MaterialTheme.colorScheme.outline)}; +// } +// } +// """.trimIndent() +// +// LaunchedEffect(htmlContent) { +// webView.loadDataWithBaseURL( +// null, +// wrapHtml(htmlContent, css), +// "text/html", +// "UTF-8", +// null +// ) +// } +// +// DisposableEffect(webView) { +// onDispose { +// webView.destroy() +// } +// } +// +// AndroidView( +// factory = { webView }, +// modifier = modifier, +// update = { view -> +// view.webViewClient = SafeWebViewClient(context) +// } +// ) +//} +// +//private class SafeWebViewClient( +// private val context: Context +//) : WebViewClient() { +// override fun shouldOverrideUrlLoading( +// view: WebView, +// request: WebResourceRequest +// ): Boolean { +// val url = request.url.toString() +// try { +// // Открываем ссылки во внешнем браузере +// context.startActivity( +// Intent(Intent.ACTION_VIEW, Uri.parse(url)) +// ) +// return true +// } catch (e: Exception) { +// // Обработка ошибок открытия ссылки +// return false +// } +// } +//} +// +//private fun wrapHtml(content: String, css: String): String { +// return """ +// +// +// +// +// +// +// +// ${content.sanitizeHTML()} +// +// +// """.trimIndent() +//} +// +//private fun colorToCss(color: Color): String { +// val argb = color.toArgb() +// return String.format("#%06X", 0xFFFFFF and argb) +//} \ No newline at end of file