Compare commits

19 Commits
Author SHA1 Message Date
Sweetbread a91bbaf129 wip: show messages 2026-03-20 17:04:26 +03:00
Sweetbread f8b10ebb34 wip 2026-03-13 17:35:34 +03:00
Sweetbread a090ff614a wip: Migrate to Trixnity 2026-02-22 00:30:11 +03:00
Sweetbread dd3b31d0b2 wip: Migrate to Trixnity 2026-02-21 21:21:57 +03:00
Sweetbread c7b5f20c06 wip: Migrate to Trixnity 2026-02-21 16:48:31 +03:00
Sweetbread 8d6a76ccb5 wip 2026-02-20 17:28:12 +03:00
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
Sweetbread 28337b1306 ref: async and thread stuff 2025-11-05 01:23:35 +03:00
Sweetbread 05d3d739e2 feat: check for internet connection before sending API requests 2025-11-05 00:06:04 +03:00
Sweetbread 5c6cd29a05 fix: change mxc regex pattern 2025-11-04 23:32:22 +03:00
Sweetbread 70d9db6cbf ref: caching joined rooms 2025-11-04 23:24:20 +03:00
Sweetbread 7026acc229 fixup! ref: reformat caching data in Rooms 2025-11-04 23:23:52 +03:00
Sweetbread fa53e0ae86 ref: remove coroutineScope 2025-11-04 20:42:11 +03:00
Sweetbread ebbc9a4b1f ref: reformat caching data in Rooms 2025-11-04 20:30:22 +03:00
Sweetbread 05fbfbac07 feat: show room avatar when is DM 2025-11-04 16:01:14 +03:00
Sweetbread 5897d31a51 ref: SharedPref to Splitties 2025-11-04 15:19:50 +03:00
Sweetbread a0bfba23cf dev: Highlight m.spaces 2025-11-03 23:52:43 +03:00
Sweetbread 00f273c866 deps: update AGP 2025-11-03 23:44:59 +03:00
27 changed files with 491 additions and 523 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>
+3
View File
@@ -49,6 +49,9 @@
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" /> <option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" /> <option name="previewFile" value="true" />
+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>
+12 -4
View File
@@ -1,13 +1,13 @@
/* /*
* Created by sweetbread * Created by sweetbread
* Copyright (c) 2025. All rights reserved. * Copyright (c) 2026. All rights reserved.
*/ */
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
id("com.google.devtools.ksp") // alias(libs.plugins.ksp)
} }
android { android {
@@ -16,7 +16,7 @@ android {
defaultConfig { defaultConfig {
applicationId = "ru.risdeveau.pixeldragon" applicationId = "ru.risdeveau.pixeldragon"
minSdk = 24 minSdk = 28
targetSdk = 35 targetSdk = 35
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
@@ -62,6 +62,13 @@ dependencies {
debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest) debugImplementation(libs.androidx.ui.test.manifest)
// Trixnity - Matrix wrapper
implementation(libs.trixnity.client)
implementation(libs.trixnity.client.media.okio)
implementation(libs.trixnity.client.repository.room)
implementation(libs.trixnity.client.cryptodriver.vodozemac)
// implementation(libs.trixnity.messenger)
// Ktor - web client // Ktor - web client
implementation(libs.ktor.client.core) implementation(libs.ktor.client.core)
implementation(libs.ktor.client.okhttp) implementation(libs.ktor.client.okhttp)
@@ -75,7 +82,7 @@ dependencies {
implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.runtime)
implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.room.ktx) implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler) // ksp(libs.androidx.room.compiler)
// Navigation Compose // Navigation Compose
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
@@ -87,4 +94,5 @@ dependencies {
// Others // Others
implementation(libs.splitties.base) // Syntax sugar implementation(libs.splitties.base) // Syntax sugar
implementation(libs.jsoup) // HTML parser implementation(libs.jsoup) // HTML parser
} }
+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>
@@ -1,24 +1,19 @@
/* /*
* Created by sweetbread * Created by sweetbread
* Copyright (c) 2025. All rights reserved. * Copyright (c) 2026. All rights reserved.
* Last modified 03.03.2025, 15:32
*/ */
package ru.risdeveau.pixeldragon package ru.risdeveau.pixeldragon
import android.content.Context
import android.util.Log import android.util.Log
import androidx.room.Room import de.connect2x.trixnity.client.MatrixClient
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.plugins.cache.HttpCache 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.getMe
import ru.risdeveau.pixeldragon.db.AppDatabase
import splitties.init.appCtx
val client = HttpClient { val webClient = HttpClient {
install(Logging) { install(Logging) {
logger = object : Logger { logger = object : Logger {
override fun log(message: String) { override fun log(message: String) {
@@ -31,28 +26,4 @@ val client = HttpClient {
install(HttpCache) install(HttpCache)
} }
val accountData = appCtx.getSharedPreferences("settings", Context.MODE_PRIVATE) var client: MatrixClient? = null
lateinit var homeserver: String
lateinit var baseUrl: String
lateinit var token: String
suspend fun initCheck(): Boolean {
Log.d("initCheck", "checking...")
if (!accountData.contains("token")) return false
if (!accountData.contains("homeserver")) return false
token = accountData.getString("token", "").toString()
homeserver = accountData.getString("homeserver", "").toString()
baseUrl = "$homeserver/_matrix/client/v3"
Log.d("initCheck", "homeserver: $homeserver")
return getMe() != null
}
val db = Room.databaseBuilder(
appCtx,
AppDatabase::class.java, "database"
).build()
@@ -1,56 +1,50 @@
/* /*
* Created by sweetbread * Created by sweetbread
* Copyright (c) 2025. All rights reserved. * Copyright (c) 2026. All rights reserved.
* Last modified 03.03.2025, 20:21
*/ */
package ru.risdeveau.pixeldragon.api package ru.risdeveau.pixeldragon.api
import io.ktor.client.request.bearerAuth
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.client.statement.bodyAsText
import org.json.JSONObject import org.json.JSONObject
import ru.risdeveau.pixeldragon.baseUrl import java.time.Instant
import ru.risdeveau.pixeldragon.client
import ru.risdeveau.pixeldragon.token
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()
) // )
} //}
@@ -1,63 +1,4 @@
/* /*
* Created by sweetbread * Created by sweetbread
* Copyright (c) 2025. All rights reserved. * Copyright (c) 2026. All rights reserved.
* Last modified 03.03.2025, 18:28
*/ */
package ru.risdeveau.pixeldragon.api
import io.ktor.client.request.bearerAuth
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode
import org.json.JSONObject
import ru.risdeveau.pixeldragon.baseUrl
import ru.risdeveau.pixeldragon.client
import ru.risdeveau.pixeldragon.db
import ru.risdeveau.pixeldragon.db.Room
import ru.risdeveau.pixeldragon.token
//fun getRooms(): List<Room> {
// return db.roomDoa().getAllJoined()
//}
//
//fun updateRooms(): List<Room> {
//
//}
suspend fun getRooms(): List<String> {
val r = client.get("$baseUrl/joined_rooms")
{ bearerAuth(token) }
val rooms = JSONObject(r.bodyAsText()).getJSONArray("joined_rooms")
return List<String>(
rooms.length()
) { i -> rooms.getString(i) }
}
suspend fun getRoom(rid: String): Room {
var room = db.roomDoa().getById(rid)
if (room == null) {
val name = getState(rid, "m.room.name", "name")
val type = getState(rid, "m.room.create", "type") ?: "m.room"
val creator = getState(rid, "m.room.create", "creator")
val avatar = getState(rid, "m.room.avatar", "url")
room = Room(rid, name, type, creator, null, avatar, null, true)
db.roomDoa().insert(room)
}
return room
}
private suspend fun getState(rid: String, state: String, key: String): String? {
val r = client.get("$baseUrl/rooms/$rid/state/$state") { bearerAuth(token) }
if (r.status != HttpStatusCode.OK) return null
val json = JSONObject(r.bodyAsText())
if (!json.has(key)) return null
return json.getString(key)
}
suspend fun getAccountData(user: String, room: String, state: String): JSONObject? {
val r = client.get("$baseUrl/user/$user/rooms/$room/account_data/$state") { bearerAuth(token) }
if (r.status != HttpStatusCode.OK) return null
return JSONObject(r.bodyAsText())
}
@@ -1,24 +1,30 @@
/* /*
* Created by sweetbread * Created by sweetbread
* Copyright (c) 2025. All rights reserved. * Copyright (c) 2026. All rights reserved.
* Last modified 03.03.2025, 15:40
*/ */
package ru.risdeveau.pixeldragon.api package ru.risdeveau.pixeldragon.api
import android.util.Log
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.bodyAsText
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import ru.risdeveau.pixeldragon.client import ru.risdeveau.pixeldragon.webClient
import ru.risdeveau.pixeldragon.homeserver //import ru.risdeveau.pixeldragon.homeserver
suspend fun getHomeserver(url: String): String? { suspend fun getHomeserver(url: String): String? {
val r = try { client.get("https://$url/.well-known/matrix/client") } val r = try { webClient.get("https://$url/.well-known/matrix/client") }
catch (_: Exception) { return null } catch (e: Exception) {
Log.w("getHomeserver", "Fail sending the request", e)
return null
}
val json = try { JSONObject(r.bodyAsText()) } val json = try { JSONObject(r.bodyAsText()) }
catch (_: JSONException) { return null } catch (e: JSONException) {
Log.w("getHomeserver", "Fail parsing the JSON", e)
return null
}
if (!json.has("m.homeserver")) return null if (!json.has("m.homeserver")) return null
@@ -28,9 +34,11 @@ suspend fun getHomeserver(url: String): String? {
return homeserver return homeserver
} }
fun mxcToUrl(mxc: String): String { //fun mxcToUrl(mxc: String): String? {
val pattern = Regex("mxc://([-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6})/([a-zA-Z]+)") // val pattern = Regex("mxc://([-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6})/([^#?]+)")
val match = pattern.find(mxc) // val match = pattern.find(mxc)
//
return "$homeserver/_matrix/client/v1/media/download/${match?.groupValues[1]}/${match?.groupValues[2]}" // if ((match?.groupValues[1] == null) or (match?.groupValues[2] == null)) return null
} //
// return "$homeserver/_matrix/client/v1/media/download/${match?.groupValues[1]}/${match?.groupValues[2]}"
//}
@@ -1,84 +1,49 @@
/* /*
* Created by sweetbread * Created by sweetbread
* Copyright (c) 2025. All rights reserved. * Copyright (c) 2026. All rights reserved.
* Last modified 03.03.2025, 15:43
*/ */
package ru.risdeveau.pixeldragon.api package ru.risdeveau.pixeldragon.api
import android.annotation.SuppressLint
import android.util.Log import android.util.Log
import io.ktor.client.request.bearerAuth import de.connect2x.trixnity.client.CryptoDriverModule
import io.ktor.client.request.get import io.ktor.http.Url
import io.ktor.client.request.post import de.connect2x.trixnity.client.MatrixClient
import io.ktor.client.request.setBody import de.connect2x.trixnity.client.create
import io.ktor.client.statement.bodyAsText import de.connect2x.trixnity.client.cryptodriver.vodozemac.vodozemac
import io.ktor.http.ContentType import de.connect2x.trixnity.clientserverapi.client.MatrixClientAuthProviderData
import io.ktor.http.HttpStatusCode import de.connect2x.trixnity.clientserverapi.client.classicLogin
import io.ktor.http.contentType import de.connect2x.trixnity.clientserverapi.model.authentication.IdentifierType
import org.json.JSONObject
import ru.risdeveau.pixeldragon.accountData
import ru.risdeveau.pixeldragon.baseUrl
import ru.risdeveau.pixeldragon.client import ru.risdeveau.pixeldragon.client
import ru.risdeveau.pixeldragon.initCheck import ru.risdeveau.pixeldragon.util.getMediaStore
import ru.risdeveau.pixeldragon.token import ru.risdeveau.pixeldragon.util.getRoomStore
import splitties.experimental.ExperimentalSplittiesApi
import splitties.init.appCtx import splitties.init.appCtx
data class Me (val userId: String, val deviceId: String)
/** @OptIn(ExperimentalSplittiesApi::class)
* This func is to validate the token
*/
suspend fun getMe(): Me? {
val r = client.get("$baseUrl/account/whoami") { bearerAuth(token) }
if (r.status != HttpStatusCode.OK) {
Log.e("getMe", r.bodyAsText())
return null
}
val json = JSONObject(r.bodyAsText())
return Me(json.getString("user_id"), json.getString("device_id"))
}
@SuppressLint("ApplySharedPref")
suspend fun login(server: String, login: String, pass: String): Boolean { suspend fun login(server: String, login: String, pass: String): Boolean {
val homeserver = getHomeserver(server)!! val hs = Url(getHomeserver(server)!!)
val pinfo = appCtx.packageManager.getPackageInfo(appCtx.packageName, 0) val pinfo = appCtx.packageManager.getPackageInfo(appCtx.packageName, 0)
val pattern = """ try {
{ client = MatrixClient.create(
"type": "m.login.password", repositoriesModule = getRoomStore(appCtx),
"identifier": { mediaStoreModule = getMediaStore(),
"type": "m.id.user" cryptoDriverModule = CryptoDriverModule.vodozemac(),
}, authProviderData = MatrixClientAuthProviderData.classicLogin(
"initial_device_display_name": "PixelDragon Android v${pinfo.versionName}" baseUrl = hs,
} identifier = IdentifierType.User(login),
""".trimIndent() password = pass,
val json = JSONObject(pattern) initialDeviceDisplayName = "PixelDragon Android v${pinfo.versionName}",
json.getJSONObject("identifier").put("user", login) ).getOrThrow()
json.put("password", pass) ).getOrThrow()
val r = try { return true
client.post("$homeserver/_matrix/client/v3/login") {
setBody(json.toString())
contentType(ContentType.Application.Json)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e("login", e.toString()) Log.i("Login", "Failed to login", e)
}
return false return false
}
if (r.status != HttpStatusCode.OK) {
Log.e("login", r.bodyAsText())
return false // TODO: Inform a user of error code
}
val res = JSONObject(r.bodyAsText())
val editor = accountData.edit()
editor.putString("token", res.getString("access_token"))
editor.putString("homeserver", homeserver)
editor.commit()
return initCheck()
} }
+4
View File
@@ -0,0 +1,4 @@
/*
* Created by sweetbread
* Copyright (c) 2026. All rights reserved.
*/
+4
View File
@@ -0,0 +1,4 @@
/*
* Created by sweetbread
* Copyright (c) 2026. All rights reserved.
*/
@@ -1,81 +1,4 @@
/* /*
* Created by sweetbread * Created by sweetbread
* Copyright (c) 2025. All rights reserved. * Copyright (c) 2026. All rights reserved.
* Last modified 22.02.2025, 19:49
*/ */
package ru.risdeveau.pixeldragon.db
import androidx.room.Dao
import androidx.room.Database
import androidx.room.Delete
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.Junction
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.Relation
import androidx.room.RoomDatabase
@Entity
data class Room(
@PrimaryKey val roomId: String,
val name: String?,
val type: String,
val creatorId: String?,
val createTime: Long?,
val avatarUrl: String?,
val members: Int?,
val joined: Boolean
)
@Dao
interface RoomDao {
// @Query("SELECT * FROM room")
// fun getAll(): List<User>
// @Query("SELECT * FROM user WHERE uid IN (:userIds)")
// fun loadAllByIds(userIds: IntArray): List<User>
// @Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
// "last_name LIKE :last LIMIT 1")
// fun findByName(first: String, last: String): User
@Query("SELECT * FROM room WHERE roomId LIKE :rid LIMIT 1")
fun getById(rid: String): Room?
// @Transaction
// @Query("SELECT * FROM room WHERE ")
// fun getSpace(rid: String): Space
@Query("SELECT * FROM room WHERE joined = 1 AND roomId NOT IN (SELECT roomId FROM SpaceToRoom)")
fun getAllJoined(): List<Room>
@Insert
fun insert(vararg rooms: Room)
@Delete
fun delete(room: Room)
}
@Entity(primaryKeys = ["spaceId", "roomId"])
data class SpaceToRoom(
val spaceId: String,
val roomId: String
)
data class Space(
@Embedded val space: Room,
@Relation(
parentColumn = "spaceId",
entityColumn = "roomId",
associateBy = Junction(SpaceToRoom::class)
)
val children: List<Room>
)
@Database(entities = [Room::class, SpaceToRoom::class, User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun roomDoa(): RoomDao
}
@@ -1,18 +1,4 @@
/* /*
* Created by sweetbread on 22.02.2025, 15:45 * Created by sweetbread
* Copyright (c) 2025. All rights reserved. * Copyright (c) 2026. All rights reserved.
* Last modified 21.02.2025, 13:38
*/ */
package ru.risdeveau.pixeldragon.db
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class User(
@PrimaryKey val uid: String,
@ColumnInfo(name = "name") val name: String?,
@ColumnInfo(name = "avatar") val avatar: String?
)
+4
View File
@@ -0,0 +1,4 @@
/*
* Created by sweetbread
* Copyright (c) 2026. All rights reserved.
*/
+4
View File
@@ -0,0 +1,4 @@
/*
* Created by sweetbread
* Copyright (c) 2026. All rights reserved.
*/
@@ -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
@@ -1,38 +1,53 @@
/* /*
* Created by sweetbread * Created by sweetbread
* Copyright (c) 2025. All rights reserved. * Copyright (c) 2026. All rights reserved.
*/ */
package ru.risdeveau.pixeldragon.ui.activity package ru.risdeveau.pixeldragon.ui.activity
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument import androidx.navigation.navArgument
import de.connect2x.trixnity.client.CryptoDriverModule
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.launch
import ru.risdeveau.pixeldragon.api.Me import de.connect2x.trixnity.client.MatrixClient
import ru.risdeveau.pixeldragon.api.getMe import de.connect2x.trixnity.client.create
import ru.risdeveau.pixeldragon.ui.layout.Room import de.connect2x.trixnity.client.cryptodriver.vodozemac.vodozemac
import de.connect2x.trixnity.clientserverapi.client.SyncState
import ru.risdeveau.pixeldragon.R
import ru.risdeveau.pixeldragon.client
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 ru.risdeveau.pixeldragon.util.getMediaStore
import ru.risdeveau.pixeldragon.util.getRoomStore
import splitties.activities.start
import splitties.init.appCtx
import splitties.resources.str
var ME: Me? = null
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -42,44 +57,87 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
PixelDragonTheme { PixelDragonTheme {
var isClientReady by remember { mutableStateOf(false) }
val syncState by remember { mutableStateOf(SyncState.STOPPED) }
if (!isClientReady) {
CircularProgressIndicator()
} else {
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
topBar = { topBar = {
TopAppBar( CenterAlignedTopAppBar(
colors = topAppBarColors( colors = topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer, containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.primary, titleContentColor = MaterialTheme.colorScheme.primary,
), ),
title = { title = {
Text("Top app bar") when (syncState) {
SyncState.STARTED -> Text("Syncing...")
SyncState.INITIAL_SYNC -> Text("Initial sync...")
SyncState.STOPPED,
SyncState.RUNNING -> Text(str(R.string.app_name))
SyncState.TIMEOUT -> Text("No network connection")
SyncState.ERROR -> Text("Error syncing")
}
} }
) )
}, },
) { innerPadding -> ) { innerPadding ->
val navController = rememberNavController() val navController = rememberNavController()
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
ME = getMe()
}
}
NavHost(navController = navController, startDestination = "rooms") { NavHost(navController = navController, startDestination = "rooms") {
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}",
arguments = listOf(navArgument("rid") { type = NavType.StringType }) arguments = listOf(navArgument("rid") { type = NavType.StringType })
) { navBackStackEntry -> ) { navBackStackEntry ->
Text(modifier = Modifier.padding(innerPadding), text = "Not implemented") } Text(
modifier = Modifier.padding(innerPadding),
text = "Not implemented"
)
} }
} }
} }
} }
LaunchedEffect(Unit) {
if (client == null) {
client = MatrixClient.create(
repositoriesModule = getRoomStore(appCtx),
mediaStoreModule = getMediaStore(),
cryptoDriverModule = CryptoDriverModule.vodozemac()
).getOrNull()
if (client == null) {
start<Login>()
finish()
return@LaunchedEffect
}
}
Log.i("MainActivity", "Log in as ${client!!.userId}")
client!!.startSync()
isClientReady = true
}
}
}
}
override fun onDestroy() {
super.onDestroy()
CoroutineScope(Dispatchers.Main).launch {
client?.stopSync()
}
} }
} }
@@ -1,6 +1,6 @@
/* /*
* Created by sweetbread * Created by sweetbread
* Copyright (c) 2025. All rights reserved. * Copyright (c) 2026. All rights reserved.
*/ */
package ru.risdeveau.pixeldragon.ui.item package ru.risdeveau.pixeldragon.ui.item
@@ -13,19 +13,22 @@ import androidx.compose.material.icons.outlined.Warning
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import coil3.compose.AsyncImagePainter import androidx.compose.ui.platform.LocalContext
import coil3.compose.rememberAsyncImagePainter import coil3.compose.rememberAsyncImagePainter
import coil3.network.NetworkHeaders
import coil3.network.httpHeaders
import coil3.request.ImageRequest import coil3.request.ImageRequest
import ru.risdeveau.pixeldragon.api.mxcToUrl import kotlinx.coroutines.flow.MutableStateFlow
import ru.risdeveau.pixeldragon.token import de.connect2x.trixnity.client.media
import splitties.init.appCtx import de.connect2x.trixnity.clientserverapi.model.media.FileTransferProgress
import ru.risdeveau.pixeldragon.client
enum class ImageLoadState { enum class ImageLoadState {
Loading, Success, Error Loading, Success, Error
@@ -36,42 +39,91 @@ fun MXCImage(
mxcUrl: String, mxcUrl: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Fit, contentScale: ContentScale = ContentScale.Fit,
contentDescription: String = "" contentDescription: String = "",
showProgress: Boolean = true
) { ) {
val loadState = remember { mutableStateOf(ImageLoadState.Loading) } val context = LocalContext.current
val painter = rememberAsyncImagePainter( var imageLoadState by remember { mutableStateOf(ImageLoadState.Loading) }
model = ImageRequest.Builder(appCtx) var imageBytes by remember { mutableStateOf<ByteArray?>(null) }
.data(mxcToUrl(mxcUrl))
.httpHeaders( val progressFlow = remember { MutableStateFlow<FileTransferProgress?>(null) }
NetworkHeaders.Builder()
.set("Authorization", "Bearer $token") LaunchedEffect(mxcUrl) {
.set("Cache-Control", "max-age=86400") if (mxcUrl.isBlank()) {
.build() imageLoadState = ImageLoadState.Error
) return@LaunchedEffect
.build(),
onState = { state ->
loadState.value = when (state) {
is AsyncImagePainter.State.Loading -> ImageLoadState.Loading
is AsyncImagePainter.State.Success -> ImageLoadState.Success
else -> ImageLoadState.Error
}
} }
imageLoadState = ImageLoadState.Loading
progressFlow.value = null
val result = client!!.media.getMedia(
uri = mxcUrl,
progress = progressFlow.takeIf { showProgress },
saveToCache = true
) )
Box( imageLoadState = result.fold(
modifier = modifier.fillMaxSize() onSuccess = { media ->
) { val bytes = media.toByteArray()
if (bytes != null) {
imageBytes = bytes
ImageLoadState.Success
} else {
ImageLoadState.Error
}
},
onFailure = {
ImageLoadState.Error
}
)
}
val progress by progressFlow.collectAsState()
val showProgressIndicator = showProgress &&
imageLoadState == ImageLoadState.Loading &&
progress != null
val painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(context)
.data(imageBytes)
.build()
)
Box(modifier = modifier, contentAlignment = Alignment.Center) {
if (imageBytes != null) {
Image( Image(
painter = painter, painter = painter,
contentDescription = contentDescription, contentDescription = contentDescription,
contentScale = contentScale, contentScale = contentScale,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
}
when (loadState.value) { when {
ImageLoadState.Loading -> CircularProgressIndicator() showProgressIndicator -> {
ImageLoadState.Error -> Icon(Icons.Outlined.Warning, "Error") progress?.let { p ->
ImageLoadState.Success -> Unit val percent = p.total?.let { p.transferred.toFloat() / it }
if (percent == null) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
} else {
CircularProgressIndicator(
progress = percent,
modifier = Modifier.align(Alignment.Center)
)
}
} ?: CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
imageLoadState == ImageLoadState.Loading -> {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
imageLoadState == ImageLoadState.Error -> {
Icon(
Icons.Outlined.Warning,
contentDescription = "Error",
modifier = Modifier.align(Alignment.Center)
)
}
} }
} }
} }
@@ -5,6 +5,7 @@
package ru.risdeveau.pixeldragon.ui.layout package ru.risdeveau.pixeldragon.ui.layout
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@@ -28,10 +29,11 @@ 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.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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
@@ -41,43 +43,49 @@ 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 de.connect2x.trixnity.client.room
import de.connect2x.trixnity.client.room.toFlowList
import de.connect2x.trixnity.client.store.TimelineEvent
import de.connect2x.trixnity.client.user
import de.connect2x.trixnity.core.model.RoomId
import de.connect2x.trixnity.core.model.events.ClientEvent
import de.connect2x.trixnity.core.model.events.m.room.RoomMessageEventContent
import io.ktor.util.reflect.instanceOf
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapConcat
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.client
import ru.risdeveau.pixeldragon.api.getAccountData
import ru.risdeveau.pixeldragon.api.getEventsAround
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>()) } val roomId = remember(rid) { RoomId(rid) }
val coroutineScope = rememberCoroutineScope() val limit = remember { MutableStateFlow(50) }
val eventsFlow = remember(roomId) {
client!!.room
.getLastTimelineEvents(roomId)
.toFlowList(limit)
}
val eventFlows by eventsFlow.collectAsState(initial = emptyList())
val listState = rememberLazyListState() val listState = rememberLazyListState()
LaunchedEffect(true) {
coroutineScope.launch {
withContext(Dispatchers.IO) {
val readMark = getAccountData(ME!!.userId, rid, "m.fully_read")
val eventsAround = getEventsAround(rid, readMark!!.getString("event_id")) //FIXME: Null check
eventsId = eventsAround.let {
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(eventFlows, key = { it.hashCode() }) { eventFlow ->
EventItem(event) EventItem(eventFlow)
} }
if (eventFlows.isEmpty()) {
item { item {
if (eventsId.isEmpty()) { Text("Empty room", modifier = Modifier.padding(16.dp))
Text("Empty room")
} }
} }
} }
@@ -85,13 +93,15 @@ fun Room(modifier: Modifier = Modifier, rid: String) {
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun EventItem(event: Event) { fun EventItem(eventFlow: Flow<TimelineEvent>) {
Box (Modifier.fillMaxWidth()) { val event by eventFlow.collectAsState(null)
when (event.type) { event?.let { event ->
Box(Modifier.fillMaxWidth()) {
/*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
@@ -111,8 +121,11 @@ fun EventItem(event: Event) {
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"))
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"))
} }
@@ -127,13 +140,14 @@ fun EventItem(event: Event) {
} }
else -> Text(event.type, else -> Text(event.content?.getOrNull().toString(),
Modifier Modifier
.fillMaxHeight() .fillMaxHeight()
.padding(4.dp) .padding(4.dp)
.background(MaterialTheme.colorScheme.errorContainer) .background(MaterialTheme.colorScheme.errorContainer)
.padding(4.dp) .padding(4.dp)
) )*/
Text(event.content?.getOrNull().toString())
} }
} }
} }
@@ -342,3 +356,12 @@ private fun colorToCss(color: Color): String {
val argb = color.toArgb() val argb = color.toArgb()
return String.format("#%06X", 0xFFFFFF and argb) return String.format("#%06X", 0xFFFFFF and argb)
} }
@OptIn(ExperimentalCoroutinesApi::class)
fun Flow<Flow<Flow<TimelineEvent>>?>.flattenTimelineEvents(): Flow<TimelineEvent> = this
.filterNotNull()
.flatMapConcat { middleFlow ->
middleFlow.flatMapConcat { innerFlow ->
innerFlow
}
}
@@ -1,10 +1,12 @@
/* /*
* Created by sweetbread * Created by sweetbread
* Copyright (c) 2025. All rights reserved. * Copyright (c) 2026. All rights reserved.
*/ */
package ru.risdeveau.pixeldragon.ui.layout package ru.risdeveau.pixeldragon.ui.layout
import android.annotation.SuppressLint
import android.util.Log
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -18,32 +20,30 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LinearProgressIndicator
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.LaunchedEffect import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.layout.ContentScale
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import de.connect2x.trixnity.client.flattenValues
import kotlinx.coroutines.withContext import de.connect2x.trixnity.client.room
import ru.risdeveau.pixeldragon.api.getRoom import de.connect2x.trixnity.client.store.Room
import ru.risdeveau.pixeldragon.api.getRooms import de.connect2x.trixnity.client.store.type
import ru.risdeveau.pixeldragon.db.Room import de.connect2x.trixnity.core.model.events.m.room.CreateEventContent
import ru.risdeveau.pixeldragon.client
import ru.risdeveau.pixeldragon.ui.item.MXCImage import ru.risdeveau.pixeldragon.ui.item.MXCImage
@SuppressLint("FlowOperatorInvokedInComposition")
@Composable @Composable
fun RoomList(modifier: Modifier = Modifier, navController: NavController) { fun RoomList(modifier: Modifier = Modifier, navController: NavController) {
var list by remember { mutableStateOf(listOf<String>()) } val rooms by client!!.room.getAll().flattenValues().map { it.toList() }.collectAsState(initial = emptyList())
val coroutineScope = rememberCoroutineScope()
val listState = rememberLazyListState() val listState = rememberLazyListState()
// if (itemState.scrollToTop) { // if (itemState.scrollToTop) {
@@ -53,21 +53,17 @@ fun RoomList(modifier: Modifier = Modifier, navController: NavController) {
// } // }
// } // }
LaunchedEffect(true) { // LaunchedEffect(Unit) {
coroutineScope.launch { // list = withContext(Dispatchers.IO) { Room.getJoined() }
withContext(Dispatchers.IO) { // }
list = getRooms()
}
}
}
LazyColumn(modifier = modifier, state = listState) { LazyColumn(modifier = modifier, state = listState) {
items(list) { rid -> items(rooms) { room ->
RoomItem(rid = rid, navController = navController ) RoomItem(room = room, navController = navController )
} }
item { item {
if (list.isEmpty()) { if (rooms.isEmpty()) {
Text("You have no rooms") Text("You have no rooms")
} }
} }
@@ -75,67 +71,58 @@ fun RoomList(modifier: Modifier = Modifier, navController: NavController) {
} }
@Composable @Composable
fun RoomItem(modifier: Modifier = Modifier, rid: String, navController: NavController) { fun RoomItem(modifier: Modifier = Modifier, room: Room, navController: NavController) {
var room by remember { mutableStateOf<Room?>(null) } val name = room.name
val scope = rememberCoroutineScope() val isSpace = room.type == CreateEventContent.RoomType.Space
LaunchedEffect(true) {
scope.launch {
withContext(Dispatchers.IO) {
room = getRoom(rid)
}
}
}
if (room != null) {
Row( Row(
modifier modifier
.padding(8.dp) .padding(8.dp)
.height((52 + 8 * 2).dp) .height((52 + 8 * 2).dp)
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colorScheme.background) .background(
color =
if (isSpace)
MaterialTheme.colorScheme.tertiary
else
MaterialTheme.colorScheme.background
)
.clip(RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(12.dp))
.clickable { .clickable {
if (room!!.type == "m.space") if (isSpace)
navController.navigate("space/$rid") navController.navigate("space/${room.roomId}")
else else
navController.navigate("room/$rid") navController.navigate("room/${room.roomId}")
} }
.padding(8.dp) .padding(8.dp)
) { ) {
Log.v("RoomItem", room.avatarUrl.toString())
room.avatarUrl?.let { mxc ->
MXCImage( MXCImage(
room!!.avatarUrl ?: "", mxc,
modifier = Modifier modifier = Modifier
.padding(end = 4.dp) .padding(end = 4.dp)
.height(52.dp) .height(52.dp)
.width(52.dp) .width(52.dp)
.let { .let {
if (room!!.type == "m.space") if (isSpace)
it.clip(RoundedCornerShape(12.dp)) it.clip(RoundedCornerShape(12.dp))
else else
it.clip(CircleShape) it.clip(CircleShape)
} },
ContentScale.Crop
) )
}
Column { Column {
(name?.explicitName ?: name?.heroes?.firstNotNullOf {it.localpart})?.let {
Text( Text(
room!!.name ?: "Unnamed", it,
maxLines = 1, maxLines = 1,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
fontSize = MaterialTheme.typography.titleLarge.fontSize fontSize = MaterialTheme.typography.titleLarge.fontSize
) )
} }
} }
} else {
Row(
modifier
.padding(8.dp)
.height(80.dp)
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.clip(RoundedCornerShape(16.dp))
.padding(8.dp)
) {
LinearProgressIndicator(Modifier.fillMaxWidth())
}
} }
} }
@@ -0,0 +1,24 @@
/*
* Created by sweetbread
* Copyright (c) 2026. All rights reserved.
*/
package ru.risdeveau.pixeldragon.util
import android.content.Context
import androidx.room.Room
import de.connect2x.trixnity.client.MediaStoreModule
import de.connect2x.trixnity.client.RepositoriesModule
import de.connect2x.trixnity.client.media.okio.okio
import de.connect2x.trixnity.client.store.repository.room.TrixnityRoomDatabase
import de.connect2x.trixnity.client.store.repository.room.room
import okio.Path.Companion.toPath
import splitties.init.appCtx
fun getMediaStore() = MediaStoreModule.okio(appCtx.filesDir.resolve("media").absolutePath.toPath())
fun getRoomStore(context: Context) = RepositoriesModule.room(
databaseBuilder = Room.databaseBuilder<TrixnityRoomDatabase>(
context,
"trixnity.db"
)
)
+2 -4
View File
@@ -1,7 +1,6 @@
/* /*
* Created by sweetbread on 22.02.2025, 15:45 * Created by sweetbread
* Copyright (c) 2025. All rights reserved. * Copyright (c) 2026. All rights reserved.
* Last modified 21.02.2025, 12:21
*/ */
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
@@ -9,5 +8,4 @@ plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.kotlin.compose) apply false
id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
} }
+7 -5
View File
@@ -1,8 +1,8 @@
[versions] [versions]
agp = "8.7.3" agp = "8.13.0"
coil = "3.1.0" coil = "3.1.0"
jsoup = "1.20.1" jsoup = "1.20.1"
kotlin = "2.0.21" kotlin = "2.2.21"
coreKtx = "1.15.0" coreKtx = "1.15.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.2.1" junitVersion = "1.2.1"
@@ -15,6 +15,7 @@ composeBom = "2025.02.00"
navigationCompose = "2.8.8" navigationCompose = "2.8.8"
room = "2.6.1" room = "2.6.1"
splittiesFunPackAndroidBase = "3.0.0" splittiesFunPackAndroidBase = "3.0.0"
trixnityClient = "5.2.0"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -24,7 +25,6 @@ androidx-navigation-dynamic-features-fragment = { module = "androidx.navigation:
androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment", version.ref = "navigationCompose" } androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment", version.ref = "navigationCompose" }
androidx-navigation-testing = { module = "androidx.navigation:navigation-testing", version.ref = "navigationCompose" } androidx-navigation-testing = { module = "androidx.navigation:navigation-testing", version.ref = "navigationCompose" }
androidx-navigation-ui = { module = "androidx.navigation:navigation-ui", version.ref = "navigationCompose" } androidx-navigation-ui = { module = "androidx.navigation:navigation-ui", version.ref = "navigationCompose" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
@@ -43,14 +43,16 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
splitties-base = { module = "com.louiscad.splitties:splitties-fun-pack-android-base", version.ref = "splittiesFunPackAndroidBase" } splitties-base = { module = "com.louiscad.splitties:splitties-fun-pack-android-base", version.ref = "splittiesFunPackAndroidBase" }
trixnity-client = { module = "de.connect2x.trixnity:trixnity-client", version.ref = "trixnityClient" }
trixnity-client-media-okio = { module = "de.connect2x.trixnity:trixnity-client-media-okio", version.ref = "trixnityClient" }
trixnity-client-repository-room = { module = "de.connect2x.trixnity:trixnity-client-repository-room", version.ref = "trixnityClient" }
trixnity-client-cryptodriver-vodozemac = { module = "de.connect2x.trixnity:trixnity-client-cryptodriver-vodozemac", version.ref = "trixnityClient" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+2 -3
View File
@@ -1,12 +1,11 @@
# #
# Created by sweetbread on 21.02.2025, 12:00 # Created by sweetbread
# Copyright (c) 2025. All rights reserved. # Copyright (c) 2025. All rights reserved.
# Last modified 21.02.2025, 12:00
# #
#Thu Feb 20 10:45:47 GMT 2025 #Thu Feb 20 10:45:47 GMT 2025
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
+6 -3
View File
@@ -1,7 +1,6 @@
/* /*
* Created by sweetbread on 21.02.2025, 12:00 * Created by sweetbread
* Copyright (c) 2025. All rights reserved. * Copyright (c) 2026. All rights reserved.
* Last modified 21.02.2025, 12:00
*/ */
pluginManagement { pluginManagement {
@@ -17,11 +16,15 @@ pluginManagement {
gradlePluginPortal() gradlePluginPortal()
} }
} }
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
dependencyResolutionManagement { dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven("https://gitlab.com/api/v4/projects/47538655/packages/maven")
} }
} }