Compare commits

...

2 Commits

Author SHA1 Message Date
Sweetbread 1cfad2ca4e wip 2026-02-20 09:41:06 +03:00
Sweetbread b6e8c73758 ref: open Login from MainActivity, not vice versa 2025-11-08 20:40:56 +03:00
10 changed files with 328 additions and 63 deletions
+6
View File
@@ -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>
+4
View File
@@ -5,8 +5,12 @@
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<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.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>
</option>
</component>
Generated Executable
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="StudioBotProjectSettings">
<option name="shareContext" value="OptedIn" />
</component>
</project>
+3 -4
View File
@@ -1,8 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Created by sweetbread on 22.02.2025, 15:45
~ Created by sweetbread
~ Copyright (c) 2025. All rights reserved.
~ Last modified 22.02.2025, 14:00
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
@@ -21,7 +20,7 @@
android:theme="@style/Theme.PixelDragon"
tools:targetApi="31">
<activity
android:name=".ui.activity.Login"
android:name=".ui.activity.MainActivity"
android:exported="true"
android:theme="@style/Theme.PixelDragon">
<intent-filter>
@@ -32,7 +31,7 @@
</activity>
<activity
android:name=".ui.activity.MainActivity"
android:name=".ui.activity.Login"
android:exported="false"
android:theme="@style/Theme.PixelDragon" />
</application>
@@ -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,18 +32,20 @@ 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"
@@ -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<Event>,
val after: List<Event>
)
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<Event>(it.length()) { i -> Event(it.getJSONObject(i))}.reversed()
} else listOf(),
if (json.has("events_after")) json.getJSONArray("events_after").let {
List<Event>(it.length()) { i -> Event(it.getJSONObject(i))}
} else listOf()
)
}
//data class EventsAround (
// val base: Event,
// val before: List<Event>,
// val after: List<Event>
//)
//
//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<Event>(it.length()) { i -> Event(it.getJSONObject(i))}.reversed()
// } else listOf(),
// if (json.has("events_after")) json.getJSONArray("events_after").let {
// List<Event>(it.length()) { i -> Event(it.getJSONObject(i))}
// } else listOf()
// )
//}
+237
View File
@@ -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>(SyncState.Idle)
val syncState: StateFlow<SyncState> = _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<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 {
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
}
@@ -1,7 +1,6 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
* Last modified 03.03.2025, 16:08
*/
package ru.risdeveau.pixeldragon.ui.activity
@@ -36,11 +35,9 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import ru.risdeveau.pixeldragon.api.getHomeserver
import ru.risdeveau.pixeldragon.api.login
import ru.risdeveau.pixeldragon.initCheck
import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme
import splitties.activities.start
@@ -49,14 +46,6 @@ class Login : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GlobalScope.launch {
if (initCheck()) {
start<MainActivity>()
finish()
}
}
enableEdgeToEdge()
setContent {
PixelDragonTheme {
@@ -75,7 +64,10 @@ class Login : ComponentActivity() {
.padding(innerPadding)) {
LoginField(
Modifier.align(Alignment.Center),
{ start<MainActivity>() },
{
start<MainActivity>()
finish()
},
{
scope.launch {
snackbarHostState
@@ -28,9 +28,12 @@ import kotlinx.coroutines.Dispatchers
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
import splitties.activities.start
var ME: Me? = null
@@ -59,18 +62,31 @@ class MainActivity : ComponentActivity() {
val navController = rememberNavController()
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
ME = getMe()
if (initCheck()) {
ME = withContext(Dispatchers.IO) { getMe() }
if (ME != null) {
syncService.startSync()
navController.navigate("rooms")
} else {
start<Login>()
finish()
}
} else {
start<Login>()
finish()
}
}
NavHost(navController = navController, startDestination = "rooms") {
NavHost(navController = navController, startDestination = "none") {
composable("none") { }
composable("rooms") { RoomList(Modifier.padding(innerPadding), navController) }
composable(
"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}",
@@ -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 ->