Compare commits
2 Commits
dev
...
8d6a76ccb5
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d6a76ccb5 | |||
| 1cfad2ca4e |
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AndroidProjectSystem">
|
||||||
|
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+4
@@ -5,8 +5,12 @@
|
|||||||
<set>
|
<set>
|
||||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||||
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="StudioBotProjectSettings">
|
||||||
|
<option name="shareContext" value="OptedIn" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -11,6 +11,7 @@ import io.ktor.client.plugins.cache.HttpCache
|
|||||||
import io.ktor.client.plugins.logging.LogLevel
|
import io.ktor.client.plugins.logging.LogLevel
|
||||||
import io.ktor.client.plugins.logging.Logger
|
import io.ktor.client.plugins.logging.Logger
|
||||||
import io.ktor.client.plugins.logging.Logging
|
import io.ktor.client.plugins.logging.Logging
|
||||||
|
import ru.risdeveau.pixeldragon.api.MatrixSyncService
|
||||||
import ru.risdeveau.pixeldragon.api.getMe
|
import ru.risdeveau.pixeldragon.api.getMe
|
||||||
import splitties.preferences.Preferences
|
import splitties.preferences.Preferences
|
||||||
|
|
||||||
@@ -31,20 +32,23 @@ lateinit var homeserver: String
|
|||||||
lateinit var baseUrl: String
|
lateinit var baseUrl: String
|
||||||
lateinit var token: String
|
lateinit var token: String
|
||||||
|
|
||||||
object AccountData : Preferences("settings") {
|
object AccountData : Preferences("system_parameters") {
|
||||||
var token by stringPref("token", "")
|
var token by stringOrNullPref("token", null)
|
||||||
var homeserver by stringPref("homeserver", "")
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val syncService = MatrixSyncService()
|
||||||
|
|
||||||
suspend fun initCheck(): Boolean {
|
suspend fun initCheck(): Boolean {
|
||||||
Log.d("initCheck", "checking...")
|
Log.d("initCheck", "checking...")
|
||||||
|
|
||||||
token = AccountData.token
|
token = AccountData.token ?: return false
|
||||||
homeserver = AccountData.homeserver
|
homeserver = AccountData.homeserver ?: return false
|
||||||
|
|
||||||
if (token.isEmpty() or homeserver.isEmpty()) return false
|
|
||||||
|
|
||||||
baseUrl = "$homeserver/_matrix/client/v3"
|
baseUrl = "$homeserver/_matrix/client/v3"
|
||||||
|
|
||||||
return getMe() != null
|
return getMe() != null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,43 +14,45 @@ import org.json.JSONObject
|
|||||||
import ru.risdeveau.pixeldragon.baseUrl
|
import ru.risdeveau.pixeldragon.baseUrl
|
||||||
import ru.risdeveau.pixeldragon.client
|
import ru.risdeveau.pixeldragon.client
|
||||||
import ru.risdeveau.pixeldragon.token
|
import ru.risdeveau.pixeldragon.token
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
class Event (
|
class Event (
|
||||||
val id: String,
|
val id: String,
|
||||||
val rid: String,
|
val rid: String,
|
||||||
val sender: String,
|
|
||||||
val type: String,
|
val type: String,
|
||||||
val content: JSONObject
|
val content: JSONObject,
|
||||||
|
val time: Instant,
|
||||||
|
val sender: String
|
||||||
) {
|
) {
|
||||||
constructor(json: JSONObject) : this(
|
// constructor(json: JSONObject) : this(
|
||||||
json.getString("event_id"),
|
// json.getString("event_id"),
|
||||||
json.getString("room_id"),
|
// json.getString("room_id"),
|
||||||
json.getString("sender"),
|
// json.getString("sender"),
|
||||||
json.getString("type"),
|
// json.getString("type"),
|
||||||
json.getJSONObject("content")
|
// json.getJSONObject("content")
|
||||||
)
|
// )
|
||||||
}
|
}
|
||||||
|
|
||||||
data class EventsAround (
|
//data class EventsAround (
|
||||||
val base: Event,
|
// val base: Event,
|
||||||
val before: List<Event>,
|
// val before: List<Event>,
|
||||||
val after: List<Event>
|
// val after: List<Event>
|
||||||
)
|
//)
|
||||||
|
//
|
||||||
suspend fun getEventsAround(room: String, event: String): EventsAround {
|
//suspend fun getEventsAround(room: String, event: String): EventsAround {
|
||||||
val r = client.get("$baseUrl/rooms/$room/context/$event") {
|
// val r = client.get("$baseUrl/rooms/$room/context/$event") {
|
||||||
bearerAuth(token)
|
// bearerAuth(token)
|
||||||
parameter("limit", "50")
|
// parameter("limit", "50")
|
||||||
}
|
// }
|
||||||
val json = JSONObject(r.bodyAsText())
|
// val json = JSONObject(r.bodyAsText())
|
||||||
|
//
|
||||||
return EventsAround(
|
// return EventsAround(
|
||||||
Event(json.getJSONObject("event")),
|
// Event(json.getJSONObject("event")),
|
||||||
if (json.has("events_before")) json.getJSONArray("events_before").let {
|
// if (json.has("events_before")) json.getJSONArray("events_before").let {
|
||||||
List<Event>(it.length()) { i -> Event(it.getJSONObject(i))}.reversed()
|
// List<Event>(it.length()) { i -> Event(it.getJSONObject(i))}.reversed()
|
||||||
} else listOf(),
|
// } else listOf(),
|
||||||
if (json.has("events_after")) json.getJSONArray("events_after").let {
|
// if (json.has("events_after")) json.getJSONArray("events_after").let {
|
||||||
List<Event>(it.length()) { i -> Event(it.getJSONObject(i))}
|
// List<Event>(it.length()) { i -> Event(it.getJSONObject(i))}
|
||||||
} else listOf()
|
// } else listOf()
|
||||||
)
|
// )
|
||||||
}
|
//}
|
||||||
+242
@@ -0,0 +1,242 @@
|
|||||||
|
/*
|
||||||
|
* Created by sweetbread
|
||||||
|
* Copyright (c) 2025. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ru.risdeveau.pixeldragon.api
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import io.ktor.client.request.bearerAuth
|
||||||
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.request.header
|
||||||
|
import io.ktor.client.request.parameter
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
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
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
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
|
||||||
|
import ru.risdeveau.pixeldragon.db.isConnected
|
||||||
|
import ru.risdeveau.pixeldragon.token
|
||||||
|
import ru.risdeveau.pixeldragon.ui.activity.ME
|
||||||
|
import splitties.init.appCtx
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
|
class MatrixSyncService {
|
||||||
|
private val _syncState = MutableStateFlow<SyncState>(SyncState.Idle)
|
||||||
|
val syncState: StateFlow<SyncState> = _syncState.asStateFlow()
|
||||||
|
|
||||||
|
private var syncJob: Job? = null
|
||||||
|
// private var isInitialized = false
|
||||||
|
//
|
||||||
|
// fun initialize() {
|
||||||
|
// isInitialized = true
|
||||||
|
// }
|
||||||
|
|
||||||
|
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 == null) {
|
||||||
|
Log.i("MatrixSyncService", "Init syncing")
|
||||||
|
_syncState.value = SyncState.Syncing
|
||||||
|
try {
|
||||||
|
initialSync()
|
||||||
|
_syncState.value = SyncState.Idle
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("MatrixSyncService", "Initial sync error", e)
|
||||||
|
_syncState.value = SyncState.Error(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// while (isActive) {
|
||||||
|
// try {
|
||||||
|
// val response = sync (
|
||||||
|
// timeout = 30000,
|
||||||
|
// filter = getFilter()
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// processSyncResponse(response)
|
||||||
|
//
|
||||||
|
// } catch (e: Exception) {
|
||||||
|
// Log.w("sync", e.message.toString())
|
||||||
|
// delay(5000) // Wait before retry
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun stopSync() {
|
||||||
|
syncJob?.cancel()
|
||||||
|
_syncState.value = SyncState.Idle
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pauseSync() {
|
||||||
|
// Called when app goes to background
|
||||||
|
syncJob?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resumeSync() {
|
||||||
|
// Called when app comes to foreground
|
||||||
|
if (!isSyncActive()) {
|
||||||
|
startSync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isSyncActive(): Boolean = syncJob?.isActive == true
|
||||||
|
|
||||||
|
private suspend fun parseSyncResponse(response: JSONObject) {
|
||||||
|
Log.v("syncResponse", response.toString(2))
|
||||||
|
val newMessages = mutableListOf<Message>()
|
||||||
|
val roomUpdates = mutableListOf<RoomUpdate>()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class SyncState {
|
||||||
|
object Idle : SyncState()
|
||||||
|
object Syncing : SyncState()
|
||||||
|
object Success : SyncState()
|
||||||
|
data class Error(val message: String?) : SyncState()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun sync(): JSONObject {
|
||||||
|
|
||||||
|
return JSONObject()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun initialSync(): JSONObject {
|
||||||
|
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.member"
|
||||||
|
],
|
||||||
|
"lazy_load_members": true
|
||||||
|
},
|
||||||
|
"timeline": {
|
||||||
|
"limit": 0,
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"ephemeral": {
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"include_leave": false
|
||||||
|
},
|
||||||
|
"presence": {
|
||||||
|
"types": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent())
|
||||||
|
// val filterId = "vmNk"
|
||||||
|
|
||||||
|
val r = client.get("$baseUrl/sync") {
|
||||||
|
bearerAuth(token)
|
||||||
|
url {
|
||||||
|
parameter("filter", filterId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r.status != HttpStatusCode.OK)
|
||||||
|
throw IllegalStateException("Failed to sync")
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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")
|
||||||
|
}
|
||||||
@@ -26,21 +26,29 @@ import androidx.navigation.compose.rememberNavController
|
|||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import ru.risdeveau.pixeldragon.AccountData
|
||||||
import ru.risdeveau.pixeldragon.api.Me
|
import ru.risdeveau.pixeldragon.api.Me
|
||||||
import ru.risdeveau.pixeldragon.api.getMe
|
import ru.risdeveau.pixeldragon.api.getMe
|
||||||
import ru.risdeveau.pixeldragon.initCheck
|
import ru.risdeveau.pixeldragon.initCheck
|
||||||
import ru.risdeveau.pixeldragon.ui.layout.Room
|
import ru.risdeveau.pixeldragon.syncService
|
||||||
|
//import ru.risdeveau.pixeldragon.ui.layout.Room
|
||||||
import ru.risdeveau.pixeldragon.ui.layout.RoomList
|
import ru.risdeveau.pixeldragon.ui.layout.RoomList
|
||||||
import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme
|
import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme
|
||||||
import splitties.activities.start
|
import splitties.activities.start
|
||||||
|
|
||||||
var ME: Me? = null
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
if (AccountData.token == null) {
|
||||||
|
start<Login>()
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
syncService.startSync()
|
||||||
|
}
|
||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
PixelDragonTheme {
|
PixelDragonTheme {
|
||||||
@@ -60,21 +68,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
if (initCheck()) {
|
|
||||||
ME = withContext(Dispatchers.IO) { getMe() }
|
|
||||||
if (ME != null) {
|
|
||||||
navController.navigate("rooms")
|
|
||||||
} else {
|
|
||||||
start<Login>()
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
start<Login>()
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NavHost(navController = navController, startDestination = "none") {
|
NavHost(navController = navController, startDestination = "none") {
|
||||||
composable("none") { }
|
composable("none") { }
|
||||||
composable("rooms") { RoomList(Modifier.padding(innerPadding), navController) }
|
composable("rooms") { RoomList(Modifier.padding(innerPadding), navController) }
|
||||||
@@ -82,9 +75,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
"room/{rid}",
|
"room/{rid}",
|
||||||
arguments = listOf(navArgument("rid") { type = NavType.StringType })
|
arguments = listOf(navArgument("rid") { type = NavType.StringType })
|
||||||
) { navBackStackEntry ->
|
) { navBackStackEntry ->
|
||||||
Room(Modifier
|
// Room(Modifier
|
||||||
.padding(innerPadding)
|
// .padding(innerPadding)
|
||||||
.fillMaxSize(), navBackStackEntry.arguments!!.getString("rid")!!)
|
// .fillMaxSize(), navBackStackEntry.arguments!!.getString("rid")!!)
|
||||||
}
|
}
|
||||||
composable(
|
composable(
|
||||||
"space/{rid}",
|
"space/{rid}",
|
||||||
|
|||||||
@@ -1,342 +1,342 @@
|
|||||||
/*
|
///*
|
||||||
* 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.content.Context
|
||||||
import android.content.Intent
|
//import android.content.Intent
|
||||||
import android.net.Uri
|
//import android.net.Uri
|
||||||
import android.webkit.WebResourceRequest
|
//import android.webkit.WebResourceRequest
|
||||||
import android.webkit.WebView
|
//import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
//import android.webkit.WebViewClient
|
||||||
import androidx.compose.foundation.background
|
//import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
//import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
//import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
//import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
//import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
//import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
//import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
//import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
//import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
//import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
//import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
//import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
//import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
//import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
//import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
//import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
//import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
//import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
//import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
//import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
//import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
//import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
//import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
//import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.toArgb
|
//import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.platform.LocalContext
|
//import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
//import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
//import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
//import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import kotlinx.coroutines.Dispatchers
|
//import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
//import kotlinx.coroutines.withContext
|
||||||
import org.jsoup.Jsoup
|
//import org.jsoup.Jsoup
|
||||||
import org.jsoup.safety.Safelist
|
//import org.jsoup.safety.Safelist
|
||||||
import ru.risdeveau.pixeldragon.api.Event
|
//import ru.risdeveau.pixeldragon.api.Event
|
||||||
import ru.risdeveau.pixeldragon.api.getAccountData
|
//import ru.risdeveau.pixeldragon.api.getAccountData
|
||||||
import ru.risdeveau.pixeldragon.api.getEventsAround
|
//import ru.risdeveau.pixeldragon.api.getEventsAround
|
||||||
import ru.risdeveau.pixeldragon.ui.activity.ME
|
//import ru.risdeveau.pixeldragon.ui.activity.ME
|
||||||
import ru.risdeveau.pixeldragon.ui.item.MXCImage
|
//import ru.risdeveau.pixeldragon.ui.item.MXCImage
|
||||||
|
//
|
||||||
@Composable
|
//@Composable
|
||||||
fun Room(modifier: Modifier = Modifier, rid: String) {
|
//fun Room(modifier: Modifier = Modifier, rid: String) {
|
||||||
var eventsId by remember { mutableStateOf(listOf<Event>()) }
|
// var eventsId by remember { mutableStateOf(listOf<Event>()) }
|
||||||
val listState = rememberLazyListState()
|
// val listState = rememberLazyListState()
|
||||||
|
//
|
||||||
LaunchedEffect(Unit) {
|
// LaunchedEffect(Unit) {
|
||||||
withContext(Dispatchers.IO) {
|
// withContext(Dispatchers.IO) {
|
||||||
val readMark = getAccountData(ME!!.userId, rid, "m.fully_read")
|
// val readMark = getAccountData(ME!!.userId, rid, "m.fully_read")
|
||||||
val eventsAround = getEventsAround(rid, readMark!!.getString("event_id")) //FIXME: Null check
|
// val eventsAround = getEventsAround(rid, readMark!!.getString("event_id")) //FIXME: Null check
|
||||||
eventsId = eventsAround.let {
|
// eventsId = eventsAround.let {
|
||||||
it.before + listOf(it.base) + it.after
|
// it.before + listOf(it.base) + it.after
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
LazyColumn(modifier = modifier, state = listState, reverseLayout = true) {
|
// LazyColumn(modifier = modifier, state = listState, reverseLayout = true) {
|
||||||
items(eventsId.reversed()) { event ->
|
// items(eventsId.reversed()) { event ->
|
||||||
EventItem(event)
|
// EventItem(event)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
item {
|
// item {
|
||||||
if (eventsId.isEmpty()) {
|
// if (eventsId.isEmpty()) {
|
||||||
Text("Empty room")
|
// Text("Empty room")
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
//@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
//@Composable
|
||||||
fun EventItem(event: Event) {
|
//fun EventItem(event: Event) {
|
||||||
Box (Modifier.fillMaxWidth()) {
|
// Box (Modifier.fillMaxWidth()) {
|
||||||
when (event.type) {
|
// when (event.type) {
|
||||||
"m.room.message" -> Column(
|
// "m.room.message" -> Column(
|
||||||
Modifier
|
// Modifier
|
||||||
.fillMaxSize()
|
// .fillMaxSize()
|
||||||
.then(
|
// .then(
|
||||||
if (event.sender != ME!!.userId)
|
// if (event.sender != ME!!.userId)
|
||||||
Modifier.padding(end = 16.dp)
|
// Modifier.padding(end = 16.dp)
|
||||||
else
|
// else
|
||||||
Modifier.padding(start = 16.dp)
|
// Modifier.padding(start = 16.dp)
|
||||||
)
|
// )
|
||||||
.padding(4.dp)
|
// .padding(4.dp)
|
||||||
.background(
|
// .background(
|
||||||
if (event.sender != ME?.userId)
|
// if (event.sender != ME?.userId)
|
||||||
MaterialTheme.colorScheme.surfaceContainer
|
// MaterialTheme.colorScheme.surfaceContainer
|
||||||
else
|
// else
|
||||||
MaterialTheme.colorScheme.primaryContainer
|
// MaterialTheme.colorScheme.primaryContainer
|
||||||
)
|
// )
|
||||||
.clip(RoundedCornerShape(16.dp))
|
// .clip(RoundedCornerShape(16.dp))
|
||||||
.padding(4.dp)
|
// .padding(4.dp)
|
||||||
) {
|
// ) {
|
||||||
Text(event.sender, maxLines = 1, fontWeight = FontWeight.Bold)
|
// Text(event.sender, maxLines = 1, fontWeight = FontWeight.Bold)
|
||||||
|
//
|
||||||
when (val msgtype = event.content.optString("msgtype", null)) {
|
// when (val msgtype = event.content.optString("msgtype", null)) {
|
||||||
"m.text" -> when (event.content.optString("format")) {
|
// "m.text" -> when (event.content.optString("format")) {
|
||||||
"org.matrix.custom.html" -> {
|
// "org.matrix.custom.html" -> {
|
||||||
if (event.content.getString("body") == event.content.getString("formatted_body"))
|
// if (event.content.getString("body") == event.content.getString("formatted_body"))
|
||||||
Text(event.content.getString("body"))
|
// Text(event.content.getString("body"))
|
||||||
HtmlRenderer(event.content.getString("formatted_body"))
|
// HtmlRenderer(event.content.getString("formatted_body"))
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
else -> Text(event.content.getString("body"))
|
// else -> Text(event.content.getString("body"))
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
"m.image" ->
|
// "m.image" ->
|
||||||
MXCImage(event.content.getString("url"), Modifier.fillMaxWidth(.9f))
|
// MXCImage(event.content.getString("url"), Modifier.fillMaxWidth(.9f))
|
||||||
|
//
|
||||||
null -> Text(event.content.toString(2))
|
// null -> Text(event.content.toString(2))
|
||||||
|
//
|
||||||
else -> Text("Unknown type: $msgtype", color = MaterialTheme.colorScheme.error)
|
// else -> Text("Unknown type: $msgtype", color = MaterialTheme.colorScheme.error)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
else -> Text(event.type,
|
// else -> Text(event.type,
|
||||||
Modifier
|
// Modifier
|
||||||
.fillMaxHeight()
|
// .fillMaxHeight()
|
||||||
.padding(4.dp)
|
// .padding(4.dp)
|
||||||
.background(MaterialTheme.colorScheme.errorContainer)
|
// .background(MaterialTheme.colorScheme.errorContainer)
|
||||||
.padding(4.dp)
|
// .padding(4.dp)
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
private fun String.sanitizeHTML(): String {
|
//private fun String.sanitizeHTML(): String {
|
||||||
val matrixSafelist = Safelist()
|
// val matrixSafelist = Safelist()
|
||||||
.addTags(
|
// .addTags(
|
||||||
"h1", "h2", "h3", "h4", "h5", "h6",
|
// "h1", "h2", "h3", "h4", "h5", "h6",
|
||||||
"b", "i", "u", "strong", "s", "del",
|
// "b", "i", "u", "strong", "s", "del",
|
||||||
"sup", "sub", "code",
|
// "sup", "sub", "code",
|
||||||
"table", "thead", "tbody",
|
// "table", "thead", "tbody",
|
||||||
"tr", "th", "td", "ul", "ol", "li",
|
// "tr", "th", "td", "ul", "ol", "li",
|
||||||
"blockquote", "details", "summary",
|
// "blockquote", "details", "summary",
|
||||||
"em", "code", "div", "pre", "span", "img"
|
// "em", "code", "div", "pre", "span", "img"
|
||||||
)
|
|
||||||
// .addAttributes("span",
|
|
||||||
// "data-mx-bg-color", "data-mx-color",
|
|
||||||
// "data-mx-spoiler", "data-mx-maths"
|
|
||||||
// )
|
// )
|
||||||
.addAttributes("a",
|
//// .addAttributes("span",
|
||||||
"target", "href"
|
//// "data-mx-bg-color", "data-mx-color",
|
||||||
)
|
//// "data-mx-spoiler", "data-mx-maths"
|
||||||
.addAttributes("img",
|
//// )
|
||||||
"width", "height", "alt", "title", "src"
|
// .addAttributes("a",
|
||||||
)
|
// "target", "href"
|
||||||
.addAttributes("ol", "start")
|
// )
|
||||||
.addAttributes("code", "class")
|
// .addAttributes("img",
|
||||||
.addAttributes("div", "data-mx-maths")
|
// "width", "height", "alt", "title", "src"
|
||||||
|
// )
|
||||||
val doc = Jsoup.parse(this)
|
// .addAttributes("ol", "start")
|
||||||
doc.select("mx-reply").remove()
|
// .addAttributes("code", "class")
|
||||||
|
// .addAttributes("div", "data-mx-maths")
|
||||||
val out = Jsoup.clean(doc.body().toString(), matrixSafelist)
|
//
|
||||||
return out
|
// val doc = Jsoup.parse(this)
|
||||||
}
|
// doc.select("mx-reply").remove()
|
||||||
|
//
|
||||||
|
// val out = Jsoup.clean(doc.body().toString(), matrixSafelist)
|
||||||
@Composable
|
// return out
|
||||||
fun HtmlRenderer(
|
//}
|
||||||
htmlContent: String,
|
//
|
||||||
modifier: Modifier = Modifier
|
//
|
||||||
) {
|
//@Composable
|
||||||
val context = LocalContext.current
|
//fun HtmlRenderer(
|
||||||
val webView = remember { WebView(context).apply {
|
// htmlContent: String,
|
||||||
settings.apply {
|
// modifier: Modifier = Modifier
|
||||||
javaScriptEnabled = false
|
//) {
|
||||||
loadWithOverviewMode = true
|
// val context = LocalContext.current
|
||||||
useWideViewPort = true
|
// val webView = remember { WebView(context).apply {
|
||||||
}
|
// settings.apply {
|
||||||
isVerticalScrollBarEnabled = false
|
// javaScriptEnabled = false
|
||||||
setBackgroundColor(Color.Transparent.toArgb())
|
// loadWithOverviewMode = true
|
||||||
} }
|
// useWideViewPort = true
|
||||||
|
// }
|
||||||
val css = """
|
// isVerticalScrollBarEnabled = false
|
||||||
body {
|
// setBackgroundColor(Color.Transparent.toArgb())
|
||||||
font-family: -apple-system, sans-serif;
|
// } }
|
||||||
font-size: 16px;
|
//
|
||||||
line-height: 1.6;
|
// val css = """
|
||||||
color: ${colorToCss(MaterialTheme.colorScheme.onBackground)};
|
// body {
|
||||||
margin: 0;
|
// font-family: -apple-system, sans-serif;
|
||||||
padding: 0;
|
// font-size: 16px;
|
||||||
}
|
// line-height: 1.6;
|
||||||
h1, h2, h3, h4, h5, h6 {
|
// color: ${colorToCss(MaterialTheme.colorScheme.onBackground)};
|
||||||
font-weight: bold;
|
// margin: 0;
|
||||||
padding: 0;
|
// padding: 0;
|
||||||
}
|
// }
|
||||||
h1 { font-size: 24px; }
|
// h1, h2, h3, h4, h5, h6 {
|
||||||
h2 { font-size: 22px; }
|
// font-weight: bold;
|
||||||
h3 { font-size: 20px; }
|
// padding: 0;
|
||||||
h4 { font-size: 18px; }
|
// }
|
||||||
h5 { font-size: 16px; }
|
// h1 { font-size: 24px; }
|
||||||
h6 { font-size: 14px; }
|
// h2 { font-size: 22px; }
|
||||||
a {
|
// h3 { font-size: 20px; }
|
||||||
color: ${colorToCss(MaterialTheme.colorScheme.primary)};
|
// h4 { font-size: 18px; }
|
||||||
text-decoration: none;
|
// h5 { font-size: 16px; }
|
||||||
}
|
// h6 { font-size: 14px; }
|
||||||
img {
|
// a {
|
||||||
max-width: 100%;
|
// color: ${colorToCss(MaterialTheme.colorScheme.primary)};
|
||||||
height: auto;
|
// text-decoration: none;
|
||||||
display: block;
|
// }
|
||||||
margin: 12px 0;
|
// img {
|
||||||
}
|
// max-width: 100%;
|
||||||
table {
|
// height: auto;
|
||||||
width: 100%;
|
// display: block;
|
||||||
border-collapse: collapse;
|
// margin: 12px 0;
|
||||||
margin: 16px 0;
|
// }
|
||||||
}
|
// table {
|
||||||
th, td {
|
// width: 100%;
|
||||||
border: 1px solid ${colorToCss(MaterialTheme.colorScheme.outline)};
|
// border-collapse: collapse;
|
||||||
padding: 12px;
|
// margin: 16px 0;
|
||||||
text-align: left;
|
// }
|
||||||
}
|
// th, td {
|
||||||
th {
|
// border: 1px solid ${colorToCss(MaterialTheme.colorScheme.outline)};
|
||||||
background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)};
|
// padding: 12px;
|
||||||
font-weight: bold;
|
// text-align: left;
|
||||||
}
|
// }
|
||||||
blockquote {
|
// th {
|
||||||
border-left: 4px solid ${colorToCss(MaterialTheme.colorScheme.primary)};
|
// background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)};
|
||||||
padding-left: 16px;
|
// font-weight: bold;
|
||||||
margin-left: 0;
|
// }
|
||||||
color: ${colorToCss(MaterialTheme.colorScheme.onSurfaceVariant)};
|
// blockquote {
|
||||||
}
|
// border-left: 4px solid ${colorToCss(MaterialTheme.colorScheme.primary)};
|
||||||
pre {
|
// padding-left: 16px;
|
||||||
background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)};
|
// margin-left: 0;
|
||||||
padding: 16px;
|
// color: ${colorToCss(MaterialTheme.colorScheme.onSurfaceVariant)};
|
||||||
overflow: auto;
|
// }
|
||||||
border-radius: 4px;
|
// pre {
|
||||||
}
|
// background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)};
|
||||||
code {
|
// padding: 16px;
|
||||||
font-family: monospace;
|
// overflow: auto;
|
||||||
background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)};
|
// border-radius: 4px;
|
||||||
padding: 2px 4px;
|
// }
|
||||||
border-radius: 4px;
|
// code {
|
||||||
}
|
// font-family: monospace;
|
||||||
hr {
|
// background-color: ${colorToCss(MaterialTheme.colorScheme.surfaceVariant)};
|
||||||
border: 0;
|
// padding: 2px 4px;
|
||||||
height: 1px;
|
// border-radius: 4px;
|
||||||
background-color: ${colorToCss(MaterialTheme.colorScheme.outline)};
|
// }
|
||||||
margin: 24px 0;
|
// hr {
|
||||||
}
|
// border: 0;
|
||||||
ul, ol {
|
// height: 1px;
|
||||||
padding-left: 24px;
|
// background-color: ${colorToCss(MaterialTheme.colorScheme.outline)};
|
||||||
margin: 12px 0;
|
// margin: 24px 0;
|
||||||
}
|
// }
|
||||||
li {
|
// ul, ol {
|
||||||
margin-bottom: 8px;
|
// padding-left: 24px;
|
||||||
}
|
// margin: 12px 0;
|
||||||
details {
|
// }
|
||||||
margin: 12px 0;
|
// li {
|
||||||
}
|
// margin-bottom: 8px;
|
||||||
summary {
|
// }
|
||||||
font-weight: bold;
|
// details {
|
||||||
cursor: pointer;
|
// margin: 12px 0;
|
||||||
}
|
// }
|
||||||
@media (prefers-color-scheme: dark) {
|
// summary {
|
||||||
:root {
|
// font-weight: bold;
|
||||||
--border-color: ${colorToCss(MaterialTheme.colorScheme.outline)};
|
// cursor: pointer;
|
||||||
}
|
// }
|
||||||
}
|
// @media (prefers-color-scheme: dark) {
|
||||||
""".trimIndent()
|
// :root {
|
||||||
|
// --border-color: ${colorToCss(MaterialTheme.colorScheme.outline)};
|
||||||
LaunchedEffect(htmlContent) {
|
// }
|
||||||
webView.loadDataWithBaseURL(
|
// }
|
||||||
null,
|
// """.trimIndent()
|
||||||
wrapHtml(htmlContent, css),
|
//
|
||||||
"text/html",
|
// LaunchedEffect(htmlContent) {
|
||||||
"UTF-8",
|
// webView.loadDataWithBaseURL(
|
||||||
null
|
// null,
|
||||||
)
|
// wrapHtml(htmlContent, css),
|
||||||
}
|
// "text/html",
|
||||||
|
// "UTF-8",
|
||||||
DisposableEffect(webView) {
|
// null
|
||||||
onDispose {
|
// )
|
||||||
webView.destroy()
|
// }
|
||||||
}
|
//
|
||||||
}
|
// DisposableEffect(webView) {
|
||||||
|
// onDispose {
|
||||||
AndroidView(
|
// webView.destroy()
|
||||||
factory = { webView },
|
// }
|
||||||
modifier = modifier,
|
// }
|
||||||
update = { view ->
|
//
|
||||||
view.webViewClient = SafeWebViewClient(context)
|
// AndroidView(
|
||||||
}
|
// factory = { webView },
|
||||||
)
|
// modifier = modifier,
|
||||||
}
|
// update = { view ->
|
||||||
|
// view.webViewClient = SafeWebViewClient(context)
|
||||||
private class SafeWebViewClient(
|
// }
|
||||||
private val context: Context
|
// )
|
||||||
) : WebViewClient() {
|
//}
|
||||||
override fun shouldOverrideUrlLoading(
|
//
|
||||||
view: WebView,
|
//private class SafeWebViewClient(
|
||||||
request: WebResourceRequest
|
// private val context: Context
|
||||||
): Boolean {
|
//) : WebViewClient() {
|
||||||
val url = request.url.toString()
|
// override fun shouldOverrideUrlLoading(
|
||||||
try {
|
// view: WebView,
|
||||||
// Открываем ссылки во внешнем браузере
|
// request: WebResourceRequest
|
||||||
context.startActivity(
|
// ): Boolean {
|
||||||
Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
// val url = request.url.toString()
|
||||||
)
|
// try {
|
||||||
return true
|
// // Открываем ссылки во внешнем браузере
|
||||||
} catch (e: Exception) {
|
// context.startActivity(
|
||||||
// Обработка ошибок открытия ссылки
|
// 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>
|
//
|
||||||
<head>
|
//private fun wrapHtml(content: String, css: String): String {
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
// return """
|
||||||
<style>
|
// <!DOCTYPE html>
|
||||||
$css
|
// <html>
|
||||||
</style>
|
// <head>
|
||||||
</head>
|
// <meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<body>
|
// <style>
|
||||||
${content.sanitizeHTML()}
|
// $css
|
||||||
</body>
|
// </style>
|
||||||
</html>
|
// </head>
|
||||||
""".trimIndent()
|
// <body>
|
||||||
}
|
// ${content.sanitizeHTML()}
|
||||||
|
// </body>
|
||||||
private fun colorToCss(color: Color): String {
|
// </html>
|
||||||
val argb = color.toArgb()
|
// """.trimIndent()
|
||||||
return String.format("#%06X", 0xFFFFFF and argb)
|
//}
|
||||||
}
|
//
|
||||||
|
//private fun colorToCss(color: Color): String {
|
||||||
|
// val argb = color.toArgb()
|
||||||
|
// return String.format("#%06X", 0xFFFFFF and argb)
|
||||||
|
//}
|
||||||
@@ -48,9 +48,9 @@ fun RoomList(modifier: Modifier = Modifier, navController: NavController) {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
// LaunchedEffect(Unit) {
|
||||||
list = withContext(Dispatchers.IO) { Room.getJoined() }
|
// list = withContext(Dispatchers.IO) { Room.getJoined() }
|
||||||
}
|
// }
|
||||||
|
|
||||||
LazyColumn(modifier = modifier, state = listState) {
|
LazyColumn(modifier = modifier, state = listState) {
|
||||||
items(list) { room ->
|
items(list) { room ->
|
||||||
|
|||||||
Reference in New Issue
Block a user