ref: reformat caching data in Rooms

This commit is contained in:
2025-11-04 20:30:22 +03:00
parent 05fbfbac07
commit ebbc9a4b1f
9 changed files with 246 additions and 87 deletions
+1 -1
View File
@@ -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"
@@ -6,15 +6,12 @@
package ru.risdeveau.pixeldragon package ru.risdeveau.pixeldragon
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.init.appCtx
import splitties.preferences.Preferences import splitties.preferences.Preferences
val client = HttpClient { val client = HttpClient {
@@ -51,8 +48,3 @@ suspend fun initCheck(): Boolean {
return getMe() != null return getMe() != null
} }
val db = Room.databaseBuilder(
appCtx,
AppDatabase::class.java, "database"
).build()
@@ -12,8 +12,8 @@ import io.ktor.http.HttpStatusCode
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> {
@@ -34,8 +34,6 @@ suspend fun getRooms(): List<String> {
} }
suspend fun getRoom(rid: String): Room { suspend fun getRoom(rid: String): Room {
var room = db.roomDoa().getById(rid)
val direct = getAccountData(getMe()!!.userId, "m.direct") val direct = getAccountData(getMe()!!.userId, "m.direct")
var directWith = "" var directWith = ""
direct?.let { direct?.let {
@@ -51,16 +49,21 @@ suspend fun getRoom(rid: String): Room {
} }
} }
if (room == null) {
val name = getState(rid, "m.room.name", "name") val name = getState(rid, "m.room.name", "name")
val type = getState(rid, "m.room.create", "type") ?: "m.room" val type = getState(rid, "m.room.create", "type") ?: "m.room"
val creator = getState(rid, "m.room.create", "creator") val creator = getState(rid, "m.room.create", "creator")
val avatar = getState(rid, "m.room.avatar", "url") val avatar = getState(rid, "m.room.avatar", "url")
room = Room(rid, name, type, creator, null, avatar, null, true, if (directWith.isNotEmpty()) directWith else null) return Room(
db.roomDoa().insert(room) rid,
} name,
type,
return room creator,
null,
avatar,
null,
true, // TODO: insert actual value
if (directWith.isNotEmpty()) User.getById(directWith) else null
)
} }
private suspend fun getState(rid: String, state: String, key: String): String? { private suspend fun getState(rid: String, state: String, key: String): String? {
+38
View File
@@ -0,0 +1,38 @@
/*
* 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 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) }
}
}
val cacheDb = Room.databaseBuilder(
appCtx,
AppDatabase::class.java, "cache"
)
.fallbackToDestructiveMigration()
.build()
@@ -6,7 +6,6 @@
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
@@ -15,11 +14,16 @@ import androidx.room.Junction
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?,
@@ -27,36 +31,56 @@ data class Room(
val avatarUrl: String?, val avatarUrl: String?,
val members: Int?, val members: Int?,
val joined: Boolean, val joined: Boolean,
val direct: String?, 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
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 +90,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,59 @@
/* /*
* 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.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
fun insert(vararg users: UserDB)
@Delete
fun delete(user: UserDB)
}
+38
View File
@@ -0,0 +1,38 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
*/
package ru.risdeveau.pixeldragon.repo
import ru.risdeveau.pixeldragon.api.getRoom
import ru.risdeveau.pixeldragon.db.cacheDb
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 ((cachedRoom == null) or (cachedRoom?.isExpired() == true)) {
val room = getRoom(id)
val cacheRoom = room.toEntity()
cacheDb.roomDoa().insert(cacheRoom)
return room
}
return cachedRoom!!.toDomain()
}
}
}
+43
View File
@@ -0,0 +1,43 @@
/*
* Created by sweetbread
* Copyright (c) 2025. All rights reserved.
*/
package ru.risdeveau.pixeldragon.repo
import android.util.Log
import org.json.JSONObject
import ru.risdeveau.pixeldragon.api.getUserProfile
import ru.risdeveau.pixeldragon.db.cacheDb
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): User? {
val cachedUser = cacheDb.userDoa().getById(id)
if ((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
)
cacheDb.userDoa().insert(user.toEntity())
return user
}
return cachedUser!!.toDomain()
}
}
}
@@ -35,11 +35,8 @@ import androidx.navigation.NavController
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import ru.risdeveau.pixeldragon.api.UserProfile
import ru.risdeveau.pixeldragon.api.getRoom
import ru.risdeveau.pixeldragon.api.getRooms import ru.risdeveau.pixeldragon.api.getRooms
import ru.risdeveau.pixeldragon.api.getUserProfile import ru.risdeveau.pixeldragon.repo.Room
import ru.risdeveau.pixeldragon.db.Room
import ru.risdeveau.pixeldragon.ui.item.MXCImage import ru.risdeveau.pixeldragon.ui.item.MXCImage
@Composable @Composable
@@ -79,31 +76,19 @@ fun RoomList(modifier: Modifier = Modifier, navController: NavController) {
@Composable @Composable
fun RoomItem(modifier: Modifier = Modifier, rid: String, navController: NavController) { fun RoomItem(modifier: Modifier = Modifier, rid: String, navController: NavController) {
var room by remember { mutableStateOf<Room?>(null) } var room by remember { mutableStateOf<Room?>(null) }
var directUser by remember { mutableStateOf<UserProfile?>(null) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
LaunchedEffect(true) { LaunchedEffect(true) {
scope.launch { scope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
room = getRoom(rid) room = Room.getById(rid)
}
}
}
LaunchedEffect(room?.direct) {
room?.let {
it.direct?.let { uid ->
scope.launch {
withContext(Dispatchers.IO) {
directUser = getUserProfile(uid)
}
}
} }
} }
} }
if (room != null) { if (room != null) {
val avatarUrl = room!!.avatarUrl ?: (directUser?.avatarUrl ?: "") val room = room!!
val avatarUrl = room.avatarUrl ?: (room.direct?.avatarUrl ?: "")
Row( Row(
modifier modifier
@@ -112,14 +97,14 @@ fun RoomItem(modifier: Modifier = Modifier, rid: String, navController: NavContr
.fillMaxWidth() .fillMaxWidth()
.background( .background(
color = color =
if (room!!.type == "m.space") if (room.type == "m.space")
MaterialTheme.colorScheme.tertiary MaterialTheme.colorScheme.tertiary
else else
MaterialTheme.colorScheme.background MaterialTheme.colorScheme.background
) )
.clip(RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(12.dp))
.clickable { .clickable {
if (room!!.type == "m.space") if (room.type == "m.space")
navController.navigate("space/$rid") navController.navigate("space/$rid")
else else
navController.navigate("room/$rid") navController.navigate("room/$rid")
@@ -133,16 +118,16 @@ fun RoomItem(modifier: Modifier = Modifier, rid: String, navController: NavContr
.height(52.dp) .height(52.dp)
.width(52.dp) .width(52.dp)
.let { .let {
if (room!!.type == "m.space") if (room.type == "m.space")
it.clip(RoundedCornerShape(12.dp)) it.clip(RoundedCornerShape(12.dp))
else else
it.clip(CircleShape) it.clip(CircleShape)
} }
) )
Column { Column {
Text(room!!.type) Text(room.type)
Text( Text(
room!!.name ?: "Unnamed", room.name ?: "Unnamed",
maxLines = 1, maxLines = 1,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
fontSize = MaterialTheme.typography.titleLarge.fontSize fontSize = MaterialTheme.typography.titleLarge.fontSize