wip: show messages

This commit is contained in:
2026-03-20 17:04:26 +03:00
parent 64de39f0ca
commit 902af5e7b5
@@ -1,342 +1,367 @@
///* /*
// * Created by sweetbread * Created by sweetbread
// * Copyright (c) 2025. All rights reserved. * Copyright (c) 2025. All rights reserved.
// */ */
//
//package ru.risdeveau.pixeldragon.ui.layout package ru.risdeveau.pixeldragon.ui.layout
//
//import android.content.Context import android.annotation.SuppressLint
//import android.content.Intent import android.content.Context
//import android.net.Uri import android.content.Intent
//import android.webkit.WebResourceRequest import android.net.Uri
//import android.webkit.WebView import android.webkit.WebResourceRequest
//import android.webkit.WebViewClient import android.webkit.WebView
//import androidx.compose.foundation.background import android.webkit.WebViewClient
//import androidx.compose.foundation.layout.Box import androidx.compose.foundation.background
//import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Box
//import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.Column
//import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxHeight
//import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxSize
//import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.fillMaxWidth
//import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.layout.padding
//import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.LazyColumn
//import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.items
//import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.lazy.rememberLazyListState
//import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.foundation.shape.RoundedCornerShape
//import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ExperimentalMaterial3Api
//import androidx.compose.material3.Text import androidx.compose.material3.MaterialTheme
//import androidx.compose.runtime.Composable import androidx.compose.material3.Text
//import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Composable
//import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.DisposableEffect
//import androidx.compose.runtime.getValue import androidx.compose.runtime.LaunchedEffect
//import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.collectAsState
//import androidx.compose.runtime.remember import androidx.compose.runtime.getValue
//import androidx.compose.runtime.setValue import androidx.compose.runtime.mutableStateOf
//import androidx.compose.ui.Modifier import androidx.compose.runtime.produceState
//import androidx.compose.ui.draw.clip import androidx.compose.runtime.remember
//import androidx.compose.ui.graphics.Color import androidx.compose.runtime.setValue
//import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.Modifier
//import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.draw.clip
//import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.graphics.Color
//import androidx.compose.ui.unit.dp import androidx.compose.ui.graphics.toArgb
//import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.platform.LocalContext
//import kotlinx.coroutines.Dispatchers import androidx.compose.ui.text.font.FontWeight
//import kotlinx.coroutines.withContext import androidx.compose.ui.unit.dp
//import org.jsoup.Jsoup import androidx.compose.ui.viewinterop.AndroidView
//import org.jsoup.safety.Safelist import de.connect2x.trixnity.client.room
//import ru.risdeveau.pixeldragon.api.Event import de.connect2x.trixnity.client.room.toFlowList
//import ru.risdeveau.pixeldragon.api.getAccountData import de.connect2x.trixnity.client.store.TimelineEvent
//import ru.risdeveau.pixeldragon.api.getEventsAround import de.connect2x.trixnity.client.user
//import ru.risdeveau.pixeldragon.ui.activity.ME import de.connect2x.trixnity.core.model.RoomId
//import ru.risdeveau.pixeldragon.ui.item.MXCImage import de.connect2x.trixnity.core.model.events.ClientEvent
// import de.connect2x.trixnity.core.model.events.m.room.RoomMessageEventContent
//@Composable import io.ktor.util.reflect.instanceOf
//fun Room(modifier: Modifier = Modifier, rid: String) { import kotlinx.coroutines.Dispatchers
// var eventsId by remember { mutableStateOf(listOf<Event>()) } import kotlinx.coroutines.ExperimentalCoroutinesApi
// val listState = rememberLazyListState() import kotlinx.coroutines.flow.Flow
// import kotlinx.coroutines.flow.MutableStateFlow
// LaunchedEffect(Unit) { import kotlinx.coroutines.flow.collectLatest
// withContext(Dispatchers.IO) { import kotlinx.coroutines.flow.filterNotNull
// val readMark = getAccountData(ME!!.userId, rid, "m.fully_read") import kotlinx.coroutines.flow.first
// val eventsAround = getEventsAround(rid, readMark!!.getString("event_id")) //FIXME: Null check import kotlinx.coroutines.flow.flatMapConcat
// eventsId = eventsAround.let { import kotlinx.coroutines.withContext
// it.before + listOf(it.base) + it.after import org.jsoup.Jsoup
// } import org.jsoup.safety.Safelist
// } import ru.risdeveau.pixeldragon.client
// } import ru.risdeveau.pixeldragon.ui.item.MXCImage
//
// LazyColumn(modifier = modifier, state = listState, reverseLayout = true) { @Composable
// items(eventsId.reversed()) { event -> fun Room(modifier: Modifier = Modifier, rid: String) {
// EventItem(event) val roomId = remember(rid) { RoomId(rid) }
// } val limit = remember { MutableStateFlow(50) }
// val eventsFlow = remember(roomId) {
// item { client!!.room
// if (eventsId.isEmpty()) { .getLastTimelineEvents(roomId)
// Text("Empty room") .toFlowList(limit)
// } }
// } val eventFlows by eventsFlow.collectAsState(initial = emptyList())
// }
//} val listState = rememberLazyListState()
//
//@OptIn(ExperimentalMaterial3Api::class) LazyColumn(modifier = modifier, state = listState, reverseLayout = true) {
//@Composable items(eventFlows, key = { it.hashCode() }) { eventFlow ->
//fun EventItem(event: Event) { EventItem(eventFlow)
// Box (Modifier.fillMaxWidth()) { }
// when (event.type) {
// "m.room.message" -> Column( if (eventFlows.isEmpty()) {
// Modifier item {
// .fillMaxSize() Text("Empty room", modifier = Modifier.padding(16.dp))
// .then( }
// if (event.sender != ME!!.userId) }
// Modifier.padding(end = 16.dp) }
// else }
// Modifier.padding(start = 16.dp)
// ) @OptIn(ExperimentalMaterial3Api::class)
// .padding(4.dp) @Composable
// .background( fun EventItem(eventFlow: Flow<TimelineEvent>) {
// if (event.sender != ME?.userId) val event by eventFlow.collectAsState(null)
// MaterialTheme.colorScheme.surfaceContainer event?.let { event ->
// else Box(Modifier.fillMaxWidth()) {
// MaterialTheme.colorScheme.primaryContainer /*when (event.type) {
// ) "m.room.message" -> Column(
// .clip(RoundedCornerShape(16.dp)) Modifier
// .padding(4.dp) .fillMaxSize()
// ) { .then(
// Text(event.sender, maxLines = 1, fontWeight = FontWeight.Bold) if (event.sender != ME!!.userId)
// Modifier.padding(end = 16.dp)
// when (val msgtype = event.content.optString("msgtype", null)) { else
// "m.text" -> when (event.content.optString("format")) { Modifier.padding(start = 16.dp)
// "org.matrix.custom.html" -> { )
// if (event.content.getString("body") == event.content.getString("formatted_body")) .padding(4.dp)
// Text(event.content.getString("body")) .background(
// HtmlRenderer(event.content.getString("formatted_body")) if (event.sender != ME?.userId)
// } MaterialTheme.colorScheme.surfaceContainer
// else
// else -> Text(event.content.getString("body")) MaterialTheme.colorScheme.primaryContainer
// } )
// .clip(RoundedCornerShape(16.dp))
// "m.image" -> .padding(4.dp)
// MXCImage(event.content.getString("url"), Modifier.fillMaxWidth(.9f)) ) {
// Text(event.sender, maxLines = 1, fontWeight = FontWeight.Bold)
// null -> Text(event.content.toString(2))
// when (val msgtype = event.content.optString("msgtype", null)) {
// else -> Text("Unknown type: $msgtype", color = MaterialTheme.colorScheme.error) "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.type, }
// Modifier
// .fillMaxHeight() else -> Text(event.content.getString("body"))
// .padding(4.dp) }
// .background(MaterialTheme.colorScheme.errorContainer)
// .padding(4.dp) "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)
//private fun String.sanitizeHTML(): String { }
// val matrixSafelist = Safelist()
// .addTags( }
// "h1", "h2", "h3", "h4", "h5", "h6",
// "b", "i", "u", "strong", "s", "del", else -> Text(event.content?.getOrNull().toString(),
// "sup", "sub", "code", Modifier
// "table", "thead", "tbody", .fillMaxHeight()
// "tr", "th", "td", "ul", "ol", "li", .padding(4.dp)
// "blockquote", "details", "summary", .background(MaterialTheme.colorScheme.errorContainer)
// "em", "code", "div", "pre", "span", "img" .padding(4.dp)
)*/
Text(event.content?.getOrNull().toString())
}
}
}
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"
// ) // )
//// .addAttributes("span", .addAttributes("a",
//// "data-mx-bg-color", "data-mx-color", "target", "href"
//// "data-mx-spoiler", "data-mx-maths" )
//// ) .addAttributes("img",
// .addAttributes("a", "width", "height", "alt", "title", "src"
// "target", "href" )
// ) .addAttributes("ol", "start")
// .addAttributes("img", .addAttributes("code", "class")
// "width", "height", "alt", "title", "src" .addAttributes("div", "data-mx-maths")
// )
// .addAttributes("ol", "start") val doc = Jsoup.parse(this)
// .addAttributes("code", "class") doc.select("mx-reply").remove()
// .addAttributes("div", "data-mx-maths")
// val out = Jsoup.clean(doc.body().toString(), matrixSafelist)
// val doc = Jsoup.parse(this) return out
// doc.select("mx-reply").remove() }
//
// val out = Jsoup.clean(doc.body().toString(), matrixSafelist)
// return out @Composable
//} fun HtmlRenderer(
// htmlContent: String,
// modifier: Modifier = Modifier
//@Composable ) {
//fun HtmlRenderer( val context = LocalContext.current
// htmlContent: String, val webView = remember { WebView(context).apply {
// modifier: Modifier = Modifier settings.apply {
//) { javaScriptEnabled = false
// val context = LocalContext.current loadWithOverviewMode = true
// val webView = remember { WebView(context).apply { useWideViewPort = true
// settings.apply { }
// javaScriptEnabled = false isVerticalScrollBarEnabled = false
// loadWithOverviewMode = true setBackgroundColor(Color.Transparent.toArgb())
// useWideViewPort = true } }
// }
// isVerticalScrollBarEnabled = false val css = """
// setBackgroundColor(Color.Transparent.toArgb()) body {
// } } font-family: -apple-system, sans-serif;
// font-size: 16px;
// val css = """ line-height: 1.6;
// body { color: ${colorToCss(MaterialTheme.colorScheme.onBackground)};
// font-family: -apple-system, sans-serif; margin: 0;
// font-size: 16px; padding: 0;
// line-height: 1.6; }
// color: ${colorToCss(MaterialTheme.colorScheme.onBackground)}; h1, h2, h3, h4, h5, h6 {
// margin: 0; font-weight: bold;
// padding: 0; padding: 0;
// } }
// h1, h2, h3, h4, h5, h6 { h1 { font-size: 24px; }
// font-weight: bold; h2 { font-size: 22px; }
// padding: 0; h3 { font-size: 20px; }
// } h4 { font-size: 18px; }
// h1 { font-size: 24px; } h5 { font-size: 16px; }
// h2 { font-size: 22px; } h6 { font-size: 14px; }
// h3 { font-size: 20px; } a {
// h4 { font-size: 18px; } color: ${colorToCss(MaterialTheme.colorScheme.primary)};
// h5 { font-size: 16px; } text-decoration: none;
// h6 { font-size: 14px; } }
// a { img {
// color: ${colorToCss(MaterialTheme.colorScheme.primary)}; max-width: 100%;
// text-decoration: none; height: auto;
// } display: block;
// img { margin: 12px 0;
// max-width: 100%; }
// height: auto; table {
// display: block; width: 100%;
// margin: 12px 0; border-collapse: collapse;
// } margin: 16px 0;
// table { }
// width: 100%; th, td {
// border-collapse: collapse; border: 1px solid ${colorToCss(MaterialTheme.colorScheme.outline)};
// margin: 16px 0; padding: 12px;
// } text-align: left;
// th, td { }
// border: 1px solid ${colorToCss(MaterialTheme.colorScheme.outline)}; th {
// padding: 12px; background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)};
// text-align: left; font-weight: bold;
// } }
// th { blockquote {
// background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)}; border-left: 4px solid ${colorToCss(MaterialTheme.colorScheme.primary)};
// font-weight: bold; padding-left: 16px;
// } margin-left: 0;
// blockquote { color: ${colorToCss(MaterialTheme.colorScheme.onSurfaceVariant)};
// border-left: 4px solid ${colorToCss(MaterialTheme.colorScheme.primary)}; }
// padding-left: 16px; pre {
// margin-left: 0; background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)};
// color: ${colorToCss(MaterialTheme.colorScheme.onSurfaceVariant)}; padding: 16px;
// } overflow: auto;
// pre { border-radius: 4px;
// background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)}; }
// padding: 16px; code {
// overflow: auto; font-family: monospace;
// border-radius: 4px; background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)};
// } padding: 2px 4px;
// code { border-radius: 4px;
// font-family: monospace; }
// background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)}; hr {
// padding: 2px 4px; border: 0;
// border-radius: 4px; height: 1px;
// } background-color: ${colorToCss(MaterialTheme.colorScheme.outline)};
// hr { margin: 24px 0;
// border: 0; }
// height: 1px; ul, ol {
// background-color: ${colorToCss(MaterialTheme.colorScheme.outline)}; padding-left: 24px;
// margin: 24px 0; margin: 12px 0;
// } }
// ul, ol { li {
// padding-left: 24px; margin-bottom: 8px;
// margin: 12px 0; }
// } details {
// li { margin: 12px 0;
// margin-bottom: 8px; }
// } summary {
// details { font-weight: bold;
// margin: 12px 0; cursor: pointer;
// } }
// summary { @media (prefers-color-scheme: dark) {
// font-weight: bold; :root {
// cursor: pointer; --border-color: ${colorToCss(MaterialTheme.colorScheme.outline)};
// } }
// @media (prefers-color-scheme: dark) { }
// :root { """.trimIndent()
// --border-color: ${colorToCss(MaterialTheme.colorScheme.outline)};
// } LaunchedEffect(htmlContent) {
// } webView.loadDataWithBaseURL(
// """.trimIndent() null,
// wrapHtml(htmlContent, css),
// LaunchedEffect(htmlContent) { "text/html",
// webView.loadDataWithBaseURL( "UTF-8",
// null, null
// wrapHtml(htmlContent, css), )
// "text/html", }
// "UTF-8",
// null DisposableEffect(webView) {
// ) onDispose {
// } webView.destroy()
// }
// DisposableEffect(webView) { }
// onDispose {
// webView.destroy() AndroidView(
// } factory = { webView },
// } modifier = modifier,
// update = { view ->
// AndroidView( view.webViewClient = SafeWebViewClient(context)
// factory = { webView }, }
// modifier = modifier, )
// update = { view -> }
// view.webViewClient = SafeWebViewClient(context)
// } private class SafeWebViewClient(
// ) private val context: Context
//} ) : WebViewClient() {
// override fun shouldOverrideUrlLoading(
//private class SafeWebViewClient( view: WebView,
// private val context: Context request: WebResourceRequest
//) : WebViewClient() { ): Boolean {
// override fun shouldOverrideUrlLoading( val url = request.url.toString()
// view: WebView, try {
// request: WebResourceRequest // Открываем ссылки во внешнем браузере
// ): Boolean { context.startActivity(
// val url = request.url.toString() Intent(Intent.ACTION_VIEW, Uri.parse(url))
// try { )
// // Открываем ссылки во внешнем браузере return true
// context.startActivity( } catch (e: Exception) {
// Intent(Intent.ACTION_VIEW, Uri.parse(url)) // Обработка ошибок открытия ссылки
// ) return false
// return true }
// } catch (e: Exception) { }
// // Обработка ошибок открытия ссылки }
// return false
// } private fun wrapHtml(content: String, css: String): String {
// } return """
//} <!DOCTYPE html>
// <html>
//private fun wrapHtml(content: String, css: String): String { <head>
// return """ <meta name="viewport" content="width=device-width, initial-scale=1">
// <!DOCTYPE html> <style>
// <html> $css
// <head> </style>
// <meta name="viewport" content="width=device-width, initial-scale=1"> </head>
// <style> <body>
// $css ${content.sanitizeHTML()}
// </style> </body>
// </head> </html>
// <body> """.trimIndent()
// ${content.sanitizeHTML()} }
// </body>
// </html> private fun colorToCss(color: Color): String {
// """.trimIndent() val argb = color.toArgb()
//} return String.format("#%06X", 0xFFFFFF and argb)
// }
//private fun colorToCss(color: Color): String {
// val argb = color.toArgb() @OptIn(ExperimentalCoroutinesApi::class)
// return String.format("#%06X", 0xFFFFFF and argb) fun Flow<Flow<Flow<TimelineEvent>>?>.flattenTimelineEvents(): Flow<TimelineEvent> = this
//} .filterNotNull()
.flatMapConcat { middleFlow ->
middleFlow.flatMapConcat { innerFlow ->
innerFlow
}
}