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> <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>
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"?> <?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. ~ Copyright (c) 2025. All rights reserved.
~ Last modified 22.02.2025, 14:00
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
@@ -21,7 +20,7 @@
android:theme="@style/Theme.PixelDragon" android:theme="@style/Theme.PixelDragon"
tools:targetApi="31"> tools:targetApi="31">
<activity <activity
android:name=".ui.activity.Login" android:name=".ui.activity.MainActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.PixelDragon"> android:theme="@style/Theme.PixelDragon">
<intent-filter> <intent-filter>
@@ -32,7 +31,7 @@
</activity> </activity>
<activity <activity
android:name=".ui.activity.MainActivity" android:name=".ui.activity.Login"
android:exported="false" android:exported="false"
android:theme="@style/Theme.PixelDragon" /> android:theme="@style/Theme.PixelDragon" />
</application> </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.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,22 @@ 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 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()
) // )
} //}
+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 * Created by sweetbread
* Copyright (c) 2025. All rights reserved. * Copyright (c) 2025. All rights reserved.
* Last modified 03.03.2025, 16:08
*/ */
package ru.risdeveau.pixeldragon.ui.activity 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.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.risdeveau.pixeldragon.api.getHomeserver import ru.risdeveau.pixeldragon.api.getHomeserver
import ru.risdeveau.pixeldragon.api.login import ru.risdeveau.pixeldragon.api.login
import ru.risdeveau.pixeldragon.initCheck
import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme
import splitties.activities.start import splitties.activities.start
@@ -49,14 +46,6 @@ class Login : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
GlobalScope.launch {
if (initCheck()) {
start<MainActivity>()
finish()
}
}
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
PixelDragonTheme { PixelDragonTheme {
@@ -75,7 +64,10 @@ class Login : ComponentActivity() {
.padding(innerPadding)) { .padding(innerPadding)) {
LoginField( LoginField(
Modifier.align(Alignment.Center), Modifier.align(Alignment.Center),
{ start<MainActivity>() }, {
start<MainActivity>()
finish()
},
{ {
scope.launch { scope.launch {
snackbarHostState snackbarHostState
@@ -28,9 +28,12 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
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.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.layout.RoomList
import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme
import splitties.activities.start
var ME: Me? = null var ME: Me? = null
@@ -59,18 +62,31 @@ class MainActivity : ComponentActivity() {
val navController = rememberNavController() val navController = rememberNavController()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
withContext(Dispatchers.IO) { if (initCheck()) {
ME = getMe() 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("rooms") { RoomList(Modifier.padding(innerPadding), navController) }
composable( composable(
"room/{rid}", "room/{rid}",
arguments = listOf(navArgument("rid") { type = NavType.StringType }) arguments = listOf(navArgument("rid") { type = NavType.StringType })
) { navBackStackEntry -> ) { navBackStackEntry ->
Room(Modifier.padding(innerPadding).fillMaxSize(), navBackStackEntry.arguments!!.getString("rid")!!) Room(Modifier
.padding(innerPadding)
.fillMaxSize(), navBackStackEntry.arguments!!.getString("rid")!!)
} }
composable( composable(
"space/{rid}", "space/{rid}",
@@ -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 ->