Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28337b1306 | ||
|
|
05d3d739e2 | ||
|
|
5c6cd29a05 | ||
|
|
70d9db6cbf | ||
|
|
7026acc229 | ||
|
|
fa53e0ae86 | ||
|
|
ebbc9a4b1f | ||
|
|
05fbfbac07 | ||
|
|
5897d31a51 | ||
|
|
a0bfba23cf | ||
|
|
00f273c866 |
No files matched your search
@@ -16,7 +16,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "ru.risdeveau.pixeldragon"
|
applicationId = "ru.risdeveau.pixeldragon"
|
||||||
minSdk = 24
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2025. 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 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.api.getMe
|
||||||
import ru.risdeveau.pixeldragon.db.AppDatabase
|
import splitties.preferences.Preferences
|
||||||
import splitties.init.appCtx
|
|
||||||
|
|
||||||
val client = HttpClient {
|
val client = HttpClient {
|
||||||
install(Logging) {
|
install(Logging) {
|
||||||
@@ -31,28 +27,24 @@ val client = HttpClient {
|
|||||||
install(HttpCache)
|
install(HttpCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
val accountData = appCtx.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
lateinit var homeserver: String
|
lateinit var homeserver: String
|
||||||
lateinit var baseUrl: String
|
lateinit var baseUrl: String
|
||||||
lateinit var token: String
|
lateinit var token: String
|
||||||
|
|
||||||
|
object AccountData : Preferences("settings") {
|
||||||
|
var token by stringPref("token", "")
|
||||||
|
var homeserver by stringPref("homeserver", "")
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun initCheck(): Boolean {
|
suspend fun initCheck(): Boolean {
|
||||||
Log.d("initCheck", "checking...")
|
Log.d("initCheck", "checking...")
|
||||||
|
|
||||||
if (!accountData.contains("token")) return false
|
token = AccountData.token
|
||||||
if (!accountData.contains("homeserver")) return false
|
homeserver = AccountData.homeserver
|
||||||
|
|
||||||
|
if (token.isEmpty() or homeserver.isEmpty()) return false
|
||||||
|
|
||||||
token = accountData.getString("token", "").toString()
|
|
||||||
homeserver = accountData.getString("homeserver", "").toString()
|
|
||||||
baseUrl = "$homeserver/_matrix/client/v3"
|
baseUrl = "$homeserver/_matrix/client/v3"
|
||||||
|
|
||||||
Log.d("initCheck", "homeserver: $homeserver")
|
|
||||||
|
|
||||||
return getMe() != null
|
return getMe() != null
|
||||||
}
|
}
|
||||||
|
|
||||||
val db = Room.databaseBuilder(
|
|
||||||
appCtx,
|
|
||||||
AppDatabase::class.java, "database"
|
|
||||||
).build()
|
|
||||||
@@ -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, 18:28
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ru.risdeveau.pixeldragon.api
|
package ru.risdeveau.pixeldragon.api
|
||||||
@@ -10,11 +9,13 @@ import io.ktor.client.request.bearerAuth
|
|||||||
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 io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import org.json.JSONObject
|
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.db
|
import ru.risdeveau.pixeldragon.repo.Room
|
||||||
import ru.risdeveau.pixeldragon.db.Room
|
import ru.risdeveau.pixeldragon.repo.User
|
||||||
import ru.risdeveau.pixeldragon.token
|
import ru.risdeveau.pixeldragon.token
|
||||||
|
|
||||||
//fun getRooms(): List<Room> {
|
//fun getRooms(): List<Room> {
|
||||||
@@ -25,27 +26,60 @@ import ru.risdeveau.pixeldragon.token
|
|||||||
//
|
//
|
||||||
//}
|
//}
|
||||||
|
|
||||||
suspend fun getRooms(): List<String> {
|
suspend fun getJoinedRooms(): List<Room> {
|
||||||
|
val r = client.get("$baseUrl/joined_rooms")
|
||||||
|
{ bearerAuth(token) }
|
||||||
|
val rooms = JSONObject(r.bodyAsText()).getJSONArray("joined_rooms")
|
||||||
|
return List(
|
||||||
|
rooms.length()
|
||||||
|
) { i -> getRoom(rooms.getString(i), true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun isJoined(id: String): Boolean {
|
||||||
val r = client.get("$baseUrl/joined_rooms")
|
val r = client.get("$baseUrl/joined_rooms")
|
||||||
{ bearerAuth(token) }
|
{ bearerAuth(token) }
|
||||||
val rooms = JSONObject(r.bodyAsText()).getJSONArray("joined_rooms")
|
val rooms = JSONObject(r.bodyAsText()).getJSONArray("joined_rooms")
|
||||||
return List<String>(
|
return List<String>(
|
||||||
rooms.length()
|
rooms.length()
|
||||||
) { i -> rooms.getString(i) }
|
) { i -> rooms.getString(i) }.contains(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getRoom(rid: String): Room {
|
suspend fun getRoom(rid: String, joined: Boolean? = null): Room {
|
||||||
var room = db.roomDoa().getById(rid)
|
val direct = getAccountData(getMe()!!.userId, "m.direct")
|
||||||
if (room == null) {
|
var directWith = ""
|
||||||
val name = getState(rid, "m.room.name", "name")
|
direct?.let {
|
||||||
val type = getState(rid, "m.room.create", "type") ?: "m.room"
|
for (user in direct.keys()) {
|
||||||
val creator = getState(rid, "m.room.create", "creator")
|
val roomsWithUser = direct.getJSONArray(user)
|
||||||
val avatar = getState(rid, "m.room.avatar", "url")
|
for (i in 0 until roomsWithUser.length()) {
|
||||||
room = Room(rid, name, type, creator, null, avatar, null, true)
|
if (rid == roomsWithUser.getString(i)) {
|
||||||
db.roomDoa().insert(room)
|
directWith = user
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (directWith.isNotEmpty()) break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return room
|
return coroutineScope {
|
||||||
|
val name = async { getState(rid, "m.room.name", "name") }
|
||||||
|
val type = async { getState(rid, "m.room.create", "type") ?: "m.room" }
|
||||||
|
val creator = async { getState(rid, "m.room.create", "creator") }
|
||||||
|
val avatar = async { getState(rid, "m.room.avatar", "url") }
|
||||||
|
val joined = async { joined ?: isJoined(rid) }
|
||||||
|
val direct = async { if (directWith.isNotEmpty()) User.getById(directWith) else null }
|
||||||
|
|
||||||
|
Room(
|
||||||
|
rid,
|
||||||
|
name.await(),
|
||||||
|
type.await(),
|
||||||
|
creator.await(),
|
||||||
|
null,
|
||||||
|
avatar.await(),
|
||||||
|
null,
|
||||||
|
joined.await(),
|
||||||
|
direct.await()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getState(rid: String, state: String, key: String): String? {
|
private suspend fun getState(rid: String, state: String, key: String): String? {
|
||||||
|
|||||||
@@ -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, 15:40
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ru.risdeveau.pixeldragon.api
|
package ru.risdeveau.pixeldragon.api
|
||||||
@@ -28,9 +27,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)
|
||||||
|
|
||||||
|
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]}"
|
return "$homeserver/_matrix/client/v1/media/download/${match?.groupValues[1]}/${match?.groupValues[2]}"
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2025. 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 io.ktor.client.request.bearerAuth
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
@@ -17,14 +15,17 @@ import io.ktor.http.ContentType
|
|||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.http.contentType
|
import io.ktor.http.contentType
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import ru.risdeveau.pixeldragon.accountData
|
import ru.risdeveau.pixeldragon.AccountData
|
||||||
import ru.risdeveau.pixeldragon.baseUrl
|
import ru.risdeveau.pixeldragon.baseUrl
|
||||||
import ru.risdeveau.pixeldragon.client
|
import ru.risdeveau.pixeldragon.client
|
||||||
import ru.risdeveau.pixeldragon.initCheck
|
import ru.risdeveau.pixeldragon.initCheck
|
||||||
import ru.risdeveau.pixeldragon.token
|
import ru.risdeveau.pixeldragon.token
|
||||||
|
import splitties.experimental.ExperimentalSplittiesApi
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
|
import splitties.preferences.edit
|
||||||
|
|
||||||
data class Me (val userId: String, val deviceId: String)
|
data class Me (val userId: String, val deviceId: String)
|
||||||
|
data class UserProfile (val displayName: String, val avatarUrl: String, val other: JSONObject)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This func is to validate the token
|
* This func is to validate the token
|
||||||
@@ -40,9 +41,9 @@ suspend fun getMe(): Me? {
|
|||||||
return Me(json.getString("user_id"), json.getString("device_id"))
|
return Me(json.getString("user_id"), json.getString("device_id"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ApplySharedPref")
|
@OptIn(ExperimentalSplittiesApi::class)
|
||||||
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 = getHomeserver(server)!!
|
||||||
|
|
||||||
val pinfo = appCtx.packageManager.getPackageInfo(appCtx.packageName, 0)
|
val pinfo = appCtx.packageManager.getPackageInfo(appCtx.packageName, 0)
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ suspend fun login(server: String, login: String, pass: String): Boolean {
|
|||||||
json.put("password", pass)
|
json.put("password", pass)
|
||||||
|
|
||||||
val r = try {
|
val r = try {
|
||||||
client.post("$homeserver/_matrix/client/v3/login") {
|
client.post("$hs/_matrix/client/v3/login") {
|
||||||
setBody(json.toString())
|
setBody(json.toString())
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
}
|
}
|
||||||
@@ -75,10 +76,25 @@ suspend fun login(server: String, login: String, pass: String): Boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val res = JSONObject(r.bodyAsText())
|
val res = JSONObject(r.bodyAsText())
|
||||||
val editor = accountData.edit()
|
AccountData.edit {
|
||||||
editor.putString("token", res.getString("access_token"))
|
token = res.getString("access_token")
|
||||||
editor.putString("homeserver", homeserver)
|
homeserver = hs
|
||||||
editor.commit()
|
}
|
||||||
|
|
||||||
return initCheck()
|
return initCheck()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getAccountData(user: String, state: String): JSONObject? {
|
||||||
|
val r = client.get("$baseUrl/user/$user/account_data/$state") { bearerAuth(token) }
|
||||||
|
if (r.status != HttpStatusCode.OK) return null
|
||||||
|
return JSONObject(r.bodyAsText())
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getUserProfile(userId: String): UserProfile? {
|
||||||
|
val r = client.get("$baseUrl/profile/$userId") { bearerAuth(token) }
|
||||||
|
if (r.status != HttpStatusCode.OK) return null
|
||||||
|
val json = JSONObject(r.bodyAsText())
|
||||||
|
val name = json.optString("displayname", ""); json.remove("displayname")
|
||||||
|
val avatar = json.optString("avatar_url", ""); json.remove("avatar_url")
|
||||||
|
return UserProfile(name, avatar, json)
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Created by sweetbread
|
||||||
|
* Copyright (c) 2025. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ru.risdeveau.pixeldragon.db
|
||||||
|
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import splitties.init.appCtx
|
||||||
|
import splitties.systemservices.connectivityManager
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
@Database(entities = [RoomDB::class, SpaceToRoom::class, UserDB::class], version = 1)
|
||||||
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
abstract fun roomDoa(): RoomDao
|
||||||
|
abstract fun userDoa(): UserDao
|
||||||
|
}
|
||||||
|
|
||||||
|
class Converters {
|
||||||
|
@TypeConverter
|
||||||
|
fun fromInstant(value: Instant?): String? {
|
||||||
|
return value?.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toInstant(value: String?): Instant? {
|
||||||
|
return value?.let { Instant.parse(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isConnected(): Boolean = connectivityManager.isDefaultNetworkActive
|
||||||
|
|
||||||
|
val cacheDb = Room.databaseBuilder(
|
||||||
|
appCtx,
|
||||||
|
AppDatabase::class.java, "cache"
|
||||||
|
)
|
||||||
|
.fallbackToDestructiveMigration()
|
||||||
|
.build()
|
||||||
@@ -1,62 +1,87 @@
|
|||||||
/*
|
/*
|
||||||
* Created by sweetbread
|
* Created by sweetbread
|
||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2025. All rights reserved.
|
||||||
* Last modified 22.02.2025, 19:49
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ru.risdeveau.pixeldragon.db
|
package ru.risdeveau.pixeldragon.db
|
||||||
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Database
|
|
||||||
import androidx.room.Delete
|
import androidx.room.Delete
|
||||||
import androidx.room.Embedded
|
import androidx.room.Embedded
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.Junction
|
import androidx.room.Junction
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Relation
|
import androidx.room.Relation
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.TypeConverters
|
||||||
|
import ru.risdeveau.pixeldragon.repo.Room
|
||||||
|
import ru.risdeveau.pixeldragon.repo.User
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
@Entity
|
@Entity(tableName = "room")
|
||||||
data class Room(
|
@TypeConverters(Converters::class)
|
||||||
@PrimaryKey val roomId: String,
|
data class RoomDB (
|
||||||
|
@PrimaryKey val id: String,
|
||||||
|
val updatedAt: Instant,
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val type: String,
|
val type: String,
|
||||||
val creatorId: String?,
|
val creatorId: String?,
|
||||||
val createTime: Long?,
|
val createTime: Long?,
|
||||||
val avatarUrl: String?,
|
val avatarUrl: String?,
|
||||||
val members: Int?,
|
val members: Int?,
|
||||||
val joined: Boolean
|
val joined: Boolean,
|
||||||
|
val direct: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun RoomDB.isExpired(cacheDuration: Long = 60 * 60): Boolean {
|
||||||
|
return Instant.now().minusSeconds(cacheDuration) > updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun RoomDB.toDomain(): Room = Room(
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
type = type,
|
||||||
|
creatorId = creatorId,
|
||||||
|
createTime = createTime,
|
||||||
|
avatarUrl = avatarUrl,
|
||||||
|
members = members,
|
||||||
|
joined = joined,
|
||||||
|
direct = direct?.let { User.getById(it) }
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Room.toEntity(): RoomDB = RoomDB(
|
||||||
|
id = id,
|
||||||
|
updatedAt = Instant.now(),
|
||||||
|
name = name,
|
||||||
|
type = type,
|
||||||
|
creatorId = creatorId,
|
||||||
|
createTime = createTime,
|
||||||
|
avatarUrl = avatarUrl,
|
||||||
|
members = members,
|
||||||
|
joined = joined,
|
||||||
|
direct = direct?.id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface RoomDao {
|
interface RoomDao {
|
||||||
// @Query("SELECT * FROM room")
|
@Query("SELECT * FROM room WHERE id LIKE :id LIMIT 1")
|
||||||
// fun getAll(): List<User>
|
fun getById(id: String): RoomDB?
|
||||||
|
|
||||||
// @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
|
// @Transaction
|
||||||
// @Query("SELECT * FROM room WHERE ")
|
// @Query("SELECT * FROM room WHERE ")
|
||||||
// fun getSpace(rid: String): Space
|
// fun getSpace(rid: String): Space
|
||||||
|
|
||||||
@Query("SELECT * FROM room WHERE joined = 1 AND roomId NOT IN (SELECT roomId FROM SpaceToRoom)")
|
@Query("SELECT * FROM room WHERE joined = 1 AND id NOT IN (SELECT id FROM SpaceToRoom)")
|
||||||
fun getAllJoined(): List<Room>
|
fun getAllJoined(): List<RoomDB>
|
||||||
|
|
||||||
@Insert
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
fun insert(vararg rooms: Room)
|
fun insert(vararg rooms: RoomDB)
|
||||||
|
|
||||||
@Delete
|
@Delete
|
||||||
fun delete(room: Room)
|
fun delete(room: RoomDB)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity(primaryKeys = ["spaceId", "roomId"])
|
@Entity(primaryKeys = ["spaceId", "roomId"])
|
||||||
@@ -66,16 +91,11 @@ data class SpaceToRoom(
|
|||||||
)
|
)
|
||||||
|
|
||||||
data class Space(
|
data class Space(
|
||||||
@Embedded val space: Room,
|
@Embedded val space: RoomDB,
|
||||||
@Relation(
|
@Relation(
|
||||||
parentColumn = "spaceId",
|
parentColumn = "spaceId",
|
||||||
entityColumn = "roomId",
|
entityColumn = "roomId",
|
||||||
associateBy = Junction(SpaceToRoom::class)
|
associateBy = Junction(SpaceToRoom::class)
|
||||||
)
|
)
|
||||||
val children: List<Room>
|
val children: List<RoomDB>
|
||||||
)
|
)
|
||||||
|
|
||||||
@Database(entities = [Room::class, SpaceToRoom::class, User::class], version = 1)
|
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
|
||||||
abstract fun roomDoa(): RoomDao
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,60 @@
|
|||||||
/*
|
/*
|
||||||
* 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 21.02.2025, 13:38
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ru.risdeveau.pixeldragon.db
|
package ru.risdeveau.pixeldragon.db
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import org.json.JSONObject
|
||||||
|
import ru.risdeveau.pixeldragon.repo.User
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
@Entity
|
@Entity(tableName = "user")
|
||||||
data class User(
|
@TypeConverters(Converters::class)
|
||||||
@PrimaryKey val uid: String,
|
data class UserDB (
|
||||||
@ColumnInfo(name = "name") val name: String?,
|
@PrimaryKey val id: String,
|
||||||
@ColumnInfo(name = "avatar") val avatar: String?
|
val updatedAt: Instant,
|
||||||
)
|
val name: String?,
|
||||||
|
val avatar: String?,
|
||||||
|
val attrs: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun UserDB.isExpired(cacheDuration: Long = 60 * 60): Boolean {
|
||||||
|
return Instant.now().minusSeconds(cacheDuration) > updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
fun UserDB.toDomain(): User = User(
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
avatarUrl = avatar,
|
||||||
|
attrs = JSONObject(attrs),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun User.toEntity(): UserDB = UserDB(
|
||||||
|
id = id,
|
||||||
|
updatedAt = Instant.now(),
|
||||||
|
name = name,
|
||||||
|
avatar = avatarUrl,
|
||||||
|
attrs = attrs.toString().trim(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface UserDao {
|
||||||
|
@Query("SELECT * FROM user WHERE id LIKE :id LIMIT 1")
|
||||||
|
fun getById(id: String): UserDB?
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun insert(vararg users: UserDB)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
fun delete(user: UserDB)
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* Created by sweetbread
|
||||||
|
* Copyright (c) 2025. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ru.risdeveau.pixeldragon.repo
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import ru.risdeveau.pixeldragon.api.getJoinedRooms
|
||||||
|
import ru.risdeveau.pixeldragon.api.getRoom
|
||||||
|
import ru.risdeveau.pixeldragon.db.cacheDb
|
||||||
|
import ru.risdeveau.pixeldragon.db.isConnected
|
||||||
|
import ru.risdeveau.pixeldragon.db.isExpired
|
||||||
|
import ru.risdeveau.pixeldragon.db.toDomain
|
||||||
|
import ru.risdeveau.pixeldragon.db.toEntity
|
||||||
|
|
||||||
|
class Room (
|
||||||
|
val id: String,
|
||||||
|
val name: String?,
|
||||||
|
val type: String,
|
||||||
|
val creatorId: String?,
|
||||||
|
val createTime: Long?,
|
||||||
|
val avatarUrl: String?,
|
||||||
|
val members: Int?,
|
||||||
|
val joined: Boolean,
|
||||||
|
val direct: User?,
|
||||||
|
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
suspend fun getById(id: String, cached: Boolean = true): Room {
|
||||||
|
val cachedRoom = cacheDb.roomDoa().getById(id)
|
||||||
|
if (!isConnected() and
|
||||||
|
(!cached or (cachedRoom == null) or (cachedRoom?.isExpired() == true))
|
||||||
|
) {
|
||||||
|
val room = getRoom(id)
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val cacheRoom = room.toEntity()
|
||||||
|
cacheDb.roomDoa().insert(cacheRoom)
|
||||||
|
}
|
||||||
|
return room
|
||||||
|
}
|
||||||
|
return cachedRoom!!.toDomain()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getJoined(cached: Boolean = true): List<Room> {
|
||||||
|
val cacheJoined = cacheDb.roomDoa().getAllJoined()
|
||||||
|
if (isConnected() and
|
||||||
|
(!cached or cacheJoined.isEmpty() or (cacheJoined.any { it.isExpired() }))
|
||||||
|
) {
|
||||||
|
val rooms = getJoinedRooms()
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val roomsDb = List(rooms.size) { i -> rooms[i].toEntity() }
|
||||||
|
cacheDb.roomDoa().insert(*roomsDb.toTypedArray())
|
||||||
|
}
|
||||||
|
return rooms
|
||||||
|
}
|
||||||
|
return List(cacheJoined.size) { i -> cacheJoined[i].toDomain() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Created by sweetbread
|
||||||
|
* Copyright (c) 2025. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ru.risdeveau.pixeldragon.repo
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.json.JSONObject
|
||||||
|
import ru.risdeveau.pixeldragon.api.getUserProfile
|
||||||
|
import ru.risdeveau.pixeldragon.db.cacheDb
|
||||||
|
import ru.risdeveau.pixeldragon.db.isConnected
|
||||||
|
import ru.risdeveau.pixeldragon.db.isExpired
|
||||||
|
import ru.risdeveau.pixeldragon.db.toDomain
|
||||||
|
import ru.risdeveau.pixeldragon.db.toEntity
|
||||||
|
|
||||||
|
class User (
|
||||||
|
val id: String,
|
||||||
|
val name: String?,
|
||||||
|
val avatarUrl: String?,
|
||||||
|
val attrs: JSONObject
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
suspend fun getById(id: String, cached: Boolean = true): User? {
|
||||||
|
val cachedUser = cacheDb.userDoa().getById(id)
|
||||||
|
if (isConnected() and
|
||||||
|
(!cached or (cachedUser == null) or (cachedUser?.isExpired() == true))
|
||||||
|
) {
|
||||||
|
val userProfile = getUserProfile(id)
|
||||||
|
if (userProfile == null) {
|
||||||
|
Log.i("User.getById", "User $id not found")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val user = User(
|
||||||
|
id,
|
||||||
|
userProfile.displayName,
|
||||||
|
userProfile.avatarUrl,
|
||||||
|
userProfile.other
|
||||||
|
)
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
cacheDb.userDoa().insert(user.toEntity())
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
return cachedUser!!.toDomain()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
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 coil3.compose.AsyncImagePainter
|
||||||
@@ -38,40 +39,43 @@ fun MXCImage(
|
|||||||
contentScale: ContentScale = ContentScale.Fit,
|
contentScale: ContentScale = ContentScale.Fit,
|
||||||
contentDescription: String = ""
|
contentDescription: String = ""
|
||||||
) {
|
) {
|
||||||
val loadState = remember { mutableStateOf(ImageLoadState.Loading) }
|
mxcToUrl(mxcUrl)?.let { url ->
|
||||||
val painter = rememberAsyncImagePainter(
|
val loadState = remember { mutableStateOf(ImageLoadState.Loading) }
|
||||||
model = ImageRequest.Builder(appCtx)
|
val painter = rememberAsyncImagePainter(
|
||||||
.data(mxcToUrl(mxcUrl))
|
model = ImageRequest.Builder(appCtx)
|
||||||
.httpHeaders(
|
.data(url)
|
||||||
NetworkHeaders.Builder()
|
.httpHeaders(
|
||||||
.set("Authorization", "Bearer $token")
|
NetworkHeaders.Builder()
|
||||||
.set("Cache-Control", "max-age=86400")
|
.set("Authorization", "Bearer $token")
|
||||||
.build()
|
.set("Cache-Control", "max-age=86400")
|
||||||
)
|
.build()
|
||||||
.build(),
|
)
|
||||||
onState = { state ->
|
.build(),
|
||||||
loadState.value = when (state) {
|
onState = { state ->
|
||||||
is AsyncImagePainter.State.Loading -> ImageLoadState.Loading
|
loadState.value = when (state) {
|
||||||
is AsyncImagePainter.State.Success -> ImageLoadState.Success
|
is AsyncImagePainter.State.Loading -> ImageLoadState.Loading
|
||||||
else -> ImageLoadState.Error
|
is AsyncImagePainter.State.Success -> ImageLoadState.Success
|
||||||
|
else -> ImageLoadState.Error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = modifier.fillMaxSize()
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painter,
|
|
||||||
contentDescription = contentDescription,
|
|
||||||
contentScale = contentScale,
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
when (loadState.value) {
|
Box(
|
||||||
ImageLoadState.Loading -> CircularProgressIndicator()
|
modifier = modifier.fillMaxSize(),
|
||||||
ImageLoadState.Error -> Icon(Icons.Outlined.Warning, "Error")
|
contentAlignment = Alignment.Center
|
||||||
ImageLoadState.Success -> Unit
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painter,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
contentScale = contentScale,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
|
||||||
|
when (loadState.value) {
|
||||||
|
ImageLoadState.Loading -> CircularProgressIndicator()
|
||||||
|
ImageLoadState.Error -> Icon(Icons.Outlined.Warning, "Error")
|
||||||
|
ImageLoadState.Success -> Unit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,6 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.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
|
||||||
@@ -42,7 +41,6 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
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
|
||||||
@@ -55,17 +53,14 @@ import ru.risdeveau.pixeldragon.ui.item.MXCImage
|
|||||||
@Composable
|
@Composable
|
||||||
fun Room(modifier: Modifier = Modifier, rid: String) {
|
fun Room(modifier: Modifier = Modifier, rid: String) {
|
||||||
var eventsId by remember { mutableStateOf(listOf<Event>()) }
|
var eventsId by remember { mutableStateOf(listOf<Event>()) }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
LaunchedEffect(true) {
|
LaunchedEffect(Unit) {
|
||||||
coroutineScope.launch {
|
withContext(Dispatchers.IO) {
|
||||||
withContext(Dispatchers.IO) {
|
val readMark = getAccountData(ME!!.userId, rid, "m.fully_read")
|
||||||
val readMark = getAccountData(ME!!.userId, rid, "m.fully_read")
|
val eventsAround = getEventsAround(rid, readMark!!.getString("event_id")) //FIXME: Null check
|
||||||
val eventsAround = getEventsAround(rid, readMark!!.getString("event_id")) //FIXME: Null check
|
eventsId = eventsAround.let {
|
||||||
eventsId = eventsAround.let {
|
it.before + listOf(it.base) + it.after
|
||||||
it.before + listOf(it.base) + it.after
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,7 +86,7 @@ fun EventItem(event: Event) {
|
|||||||
"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 +106,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"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ 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
|
||||||
@@ -26,24 +25,20 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.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
|
||||||
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import ru.risdeveau.pixeldragon.api.getRoom
|
import ru.risdeveau.pixeldragon.repo.Room
|
||||||
import ru.risdeveau.pixeldragon.api.getRooms
|
|
||||||
import ru.risdeveau.pixeldragon.db.Room
|
|
||||||
import ru.risdeveau.pixeldragon.ui.item.MXCImage
|
import ru.risdeveau.pixeldragon.ui.item.MXCImage
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomList(modifier: Modifier = Modifier, navController: NavController) {
|
fun RoomList(modifier: Modifier = Modifier, navController: NavController) {
|
||||||
var list by remember { mutableStateOf(listOf<String>()) }
|
var list by remember { mutableStateOf(listOf<Room>()) }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
// if (itemState.scrollToTop) {
|
// if (itemState.scrollToTop) {
|
||||||
@@ -53,17 +48,13 @@ 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(list) { room ->
|
||||||
RoomItem(rid = rid, navController = navController )
|
RoomItem(room = room, navController = navController )
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
@@ -75,67 +66,53 @@ 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 avatarUrl = room.avatarUrl ?: (room.direct?.avatarUrl ?: "")
|
||||||
val scope = rememberCoroutineScope()
|
val name = room.name ?: (room.direct?.name ?: "Unnamed")
|
||||||
|
|
||||||
LaunchedEffect(true) {
|
Row(
|
||||||
scope.launch {
|
modifier
|
||||||
withContext(Dispatchers.IO) {
|
.padding(8.dp)
|
||||||
room = getRoom(rid)
|
.height((52 + 8 * 2).dp)
|
||||||
}
|
.fillMaxWidth()
|
||||||
}
|
.background(
|
||||||
}
|
color =
|
||||||
|
if (room.type == "m.space")
|
||||||
if (room != null) {
|
MaterialTheme.colorScheme.tertiary
|
||||||
Row(
|
|
||||||
modifier
|
|
||||||
.padding(8.dp)
|
|
||||||
.height((52 + 8 * 2).dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(MaterialTheme.colorScheme.background)
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
.clickable {
|
|
||||||
if (room!!.type == "m.space")
|
|
||||||
navController.navigate("space/$rid")
|
|
||||||
else
|
else
|
||||||
navController.navigate("room/$rid")
|
MaterialTheme.colorScheme.background
|
||||||
}
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
|
||||||
MXCImage(
|
|
||||||
room!!.avatarUrl ?: "",
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(end = 4.dp)
|
|
||||||
.height(52.dp)
|
|
||||||
.width(52.dp)
|
|
||||||
.let {
|
|
||||||
if (room!!.type == "m.space")
|
|
||||||
it.clip(RoundedCornerShape(12.dp))
|
|
||||||
else
|
|
||||||
it.clip(CircleShape)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
Column {
|
.clip(RoundedCornerShape(12.dp))
|
||||||
Text(
|
.clickable {
|
||||||
room!!.name ?: "Unnamed",
|
if (room.type == "m.space")
|
||||||
maxLines = 1,
|
navController.navigate("space/${room.id}")
|
||||||
color = MaterialTheme.colorScheme.primary,
|
else
|
||||||
fontSize = MaterialTheme.typography.titleLarge.fontSize
|
navController.navigate("room/${room.id}")
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
.padding(8.dp)
|
||||||
} else {
|
) {
|
||||||
Row(
|
MXCImage(
|
||||||
modifier
|
avatarUrl,
|
||||||
.padding(8.dp)
|
modifier = Modifier
|
||||||
.height(80.dp)
|
.padding(end = 4.dp)
|
||||||
.fillMaxWidth()
|
.height(52.dp)
|
||||||
.background(MaterialTheme.colorScheme.background)
|
.width(52.dp)
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.let {
|
||||||
.padding(8.dp)
|
if (room.type == "m.space")
|
||||||
) {
|
it.clip(RoundedCornerShape(12.dp))
|
||||||
LinearProgressIndicator(Modifier.fillMaxWidth())
|
else
|
||||||
|
it.clip(CircleShape)
|
||||||
|
},
|
||||||
|
ContentScale.Crop
|
||||||
|
)
|
||||||
|
Column {
|
||||||
|
Text(room.type)
|
||||||
|
Text(
|
||||||
|
name,
|
||||||
|
maxLines = 1,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
fontSize = MaterialTheme.typography.titleLarge.fontSize
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[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.0.21"
|
||||||
|
|||||||
+2
-3
@@ -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
|
||||||
Reference in new issue
Block a user