diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100755 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml index 931b96c..16660f1 100755 --- a/.idea/runConfigurations.xml +++ b/.idea/runConfigurations.xml @@ -5,8 +5,12 @@ diff --git a/.idea/studiobot.xml b/.idea/studiobot.xml new file mode 100755 index 0000000..539e3b8 --- /dev/null +++ b/.idea/studiobot.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/Common.kt b/app/src/main/java/ru/risdeveau/pixeldragon/Common.kt index f72d1e0..57642c7 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/Common.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/Common.kt @@ -11,6 +11,7 @@ import io.ktor.client.plugins.cache.HttpCache import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging +import ru.risdeveau.pixeldragon.api.MatrixSyncService import ru.risdeveau.pixeldragon.api.getMe import splitties.preferences.Preferences @@ -31,20 +32,22 @@ lateinit var homeserver: String lateinit var baseUrl: String lateinit var token: String -object AccountData : Preferences("settings") { - var token by stringPref("token", "") - var homeserver by stringPref("homeserver", "") +object AccountData : Preferences("system_parameters") { + var token by stringOrNullPref("token", 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 { Log.d("initCheck", "checking...") - token = AccountData.token - homeserver = AccountData.homeserver - - if (token.isEmpty() or homeserver.isEmpty()) return false + token = AccountData.token ?: return false + homeserver = AccountData.homeserver ?: return false baseUrl = "$homeserver/_matrix/client/v3" return getMe() != null -} \ No newline at end of file +} diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/api/Event.kt b/app/src/main/java/ru/risdeveau/pixeldragon/api/Event.kt index 441a01d..2cda807 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/api/Event.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/api/Event.kt @@ -14,43 +14,45 @@ import org.json.JSONObject import ru.risdeveau.pixeldragon.baseUrl import ru.risdeveau.pixeldragon.client import ru.risdeveau.pixeldragon.token +import java.time.Instant class Event ( val id: String, val rid: String, - val sender: String, val type: String, - val content: JSONObject + val content: JSONObject, + val time: Instant, + val sender: String ) { - constructor(json: JSONObject) : this( - json.getString("event_id"), - json.getString("room_id"), - json.getString("sender"), - json.getString("type"), - json.getJSONObject("content") - ) +// constructor(json: JSONObject) : this( +// json.getString("event_id"), +// json.getString("room_id"), +// json.getString("sender"), +// json.getString("type"), +// json.getJSONObject("content") +// ) } -data class EventsAround ( - val base: Event, - val before: List, - val after: List -) - -suspend fun getEventsAround(room: String, event: String): EventsAround { - val r = client.get("$baseUrl/rooms/$room/context/$event") { - bearerAuth(token) - parameter("limit", "50") - } - val json = JSONObject(r.bodyAsText()) - - return EventsAround( - Event(json.getJSONObject("event")), - if (json.has("events_before")) json.getJSONArray("events_before").let { - List(it.length()) { i -> Event(it.getJSONObject(i))}.reversed() - } else listOf(), - if (json.has("events_after")) json.getJSONArray("events_after").let { - List(it.length()) { i -> Event(it.getJSONObject(i))} - } else listOf() - ) -} \ No newline at end of file +//data class EventsAround ( +// val base: Event, +// val before: List, +// val after: List +//) +// +//suspend fun getEventsAround(room: String, event: String): EventsAround { +// val r = client.get("$baseUrl/rooms/$room/context/$event") { +// bearerAuth(token) +// parameter("limit", "50") +// } +// val json = JSONObject(r.bodyAsText()) +// +// return EventsAround( +// Event(json.getJSONObject("event")), +// if (json.has("events_before")) json.getJSONArray("events_before").let { +// List(it.length()) { i -> Event(it.getJSONObject(i))}.reversed() +// } else listOf(), +// if (json.has("events_after")) json.getJSONArray("events_after").let { +// List(it.length()) { i -> Event(it.getJSONObject(i))} +// } else listOf() +// ) +//} \ No newline at end of file diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/api/sync.kt b/app/src/main/java/ru/risdeveau/pixeldragon/api/sync.kt new file mode 100755 index 0000000..d3e71d2 --- /dev/null +++ b/app/src/main/java/ru/risdeveau/pixeldragon/api/sync.kt @@ -0,0 +1,237 @@ +/* + * 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.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.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.Idle) + val syncState: StateFlow = _syncState.asStateFlow() + + private var syncJob: Job? = null +// private var isInitialized = false +// +// fun initialize() { +// isInitialized = true +// } + + fun startSync() { +// if (!isInitialized) +// throw IllegalStateException("Sync service not initialized") + + if (syncJob?.isActive == true) return + + syncJob = CoroutineScope(Dispatchers.IO).launch { + if (syncLastBatch == "") { + _syncState.value = SyncState.Syncing + try { + processSyncResponse(initialSync()) + _syncState.value = SyncState.Idle + } catch (e: Exception) { + _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 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) +// } + } +} + +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 { + val initialFilter = """ + { + "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": [] + }, + "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" + ] + } + """.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") +// 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.d("initialSync", "Response size: ${r.bodyAsText().length}") + + val json = JSONObject(r.bodyAsText()) + syncLastBatch = json.getString("next_batch") + + return json +} \ 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 04c37a7..8aa0952 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 @@ -29,6 +29,7 @@ import kotlinx.coroutines.withContext 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.RoomList import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme @@ -64,6 +65,7 @@ class MainActivity : ComponentActivity() { if (initCheck()) { ME = withContext(Dispatchers.IO) { getMe() } if (ME != null) { + syncService.startSync() navController.navigate("rooms") } else { start() diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/Rooms.kt b/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/Rooms.kt index 5ef7c2f..7dfdc52 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/Rooms.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/Rooms.kt @@ -48,9 +48,9 @@ fun RoomList(modifier: Modifier = Modifier, navController: NavController) { // } // } - LaunchedEffect(Unit) { - list = withContext(Dispatchers.IO) { Room.getJoined() } - } +// LaunchedEffect(Unit) { +// list = withContext(Dispatchers.IO) { Room.getJoined() } +// } LazyColumn(modifier = modifier, state = listState) { items(list) { room ->