diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f425c23..0cd53c1 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,7 +16,7 @@ android { defaultConfig { applicationId = "ru.risdeveau.pixeldragon" - minSdk = 24 + minSdk = 26 targetSdk = 35 versionCode = 1 versionName = "1.0" diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/Common.kt b/app/src/main/java/ru/risdeveau/pixeldragon/Common.kt index 2503c70..f72d1e0 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/Common.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/Common.kt @@ -6,15 +6,12 @@ package ru.risdeveau.pixeldragon import android.util.Log -import androidx.room.Room import io.ktor.client.HttpClient import io.ktor.client.plugins.cache.HttpCache import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging import ru.risdeveau.pixeldragon.api.getMe -import ru.risdeveau.pixeldragon.db.AppDatabase -import splitties.init.appCtx import splitties.preferences.Preferences val client = HttpClient { @@ -50,9 +47,4 @@ suspend fun initCheck(): Boolean { baseUrl = "$homeserver/_matrix/client/v3" return getMe() != null -} - -val db = Room.databaseBuilder( - appCtx, - AppDatabase::class.java, "database" -).build() \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/api/Room.kt b/app/src/main/java/ru/risdeveau/pixeldragon/api/Room.kt index 74ee808..273c8eb 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/api/Room.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/api/Room.kt @@ -12,8 +12,8 @@ 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.repo.Room +import ru.risdeveau.pixeldragon.repo.User import ru.risdeveau.pixeldragon.token //fun getRooms(): List { @@ -34,8 +34,6 @@ suspend fun getRooms(): List { } suspend fun getRoom(rid: String): Room { - var room = db.roomDoa().getById(rid) - val direct = getAccountData(getMe()!!.userId, "m.direct") var directWith = "" direct?.let { @@ -51,16 +49,21 @@ suspend fun getRoom(rid: String): Room { } } - 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, if (directWith.isNotEmpty()) directWith else null) - db.roomDoa().insert(room) - } - - return room + 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") + return Room( + rid, + name, + type, + 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? { diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/db/Common.kt b/app/src/main/java/ru/risdeveau/pixeldragon/db/Common.kt new file mode 100755 index 0000000..0decd41 --- /dev/null +++ b/app/src/main/java/ru/risdeveau/pixeldragon/db/Common.kt @@ -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() \ No newline at end of file diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/db/Room.kt b/app/src/main/java/ru/risdeveau/pixeldragon/db/Room.kt index a951e90..75de9ea 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/db/Room.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/db/Room.kt @@ -6,7 +6,6 @@ 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 @@ -15,11 +14,16 @@ import androidx.room.Junction import androidx.room.PrimaryKey import androidx.room.Query 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 -data class Room( - @PrimaryKey val roomId: String, +@Entity(tableName = "room") +@TypeConverters(Converters::class) +data class RoomDB ( + @PrimaryKey val id: String, + val updatedAt: Instant, val name: String?, val type: String, val creatorId: String?, @@ -27,36 +31,56 @@ data class Room( val avatarUrl: String?, val members: Int?, 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 interface RoomDao { -// @Query("SELECT * FROM room") -// fun getAll(): List - -// @Query("SELECT * FROM user WHERE uid IN (:userIds)") -// fun loadAllByIds(userIds: IntArray): List - -// @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? + @Query("SELECT * FROM room WHERE id LIKE :id LIMIT 1") + fun getById(id: String): RoomDB? // @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 + @Query("SELECT * FROM room WHERE joined = 1 AND id NOT IN (SELECT id FROM SpaceToRoom)") + fun getAllJoined(): List @Insert - fun insert(vararg rooms: Room) + fun insert(vararg rooms: RoomDB) @Delete - fun delete(room: Room) + fun delete(room: RoomDB) } @Entity(primaryKeys = ["spaceId", "roomId"]) @@ -66,16 +90,11 @@ data class SpaceToRoom( ) data class Space( - @Embedded val space: Room, + @Embedded val space: RoomDB, @Relation( parentColumn = "spaceId", entityColumn = "roomId", associateBy = Junction(SpaceToRoom::class) ) - val children: List -) - -@Database(entities = [Room::class, SpaceToRoom::class, User::class], version = 1) -abstract class AppDatabase : RoomDatabase() { - abstract fun roomDoa(): RoomDao -} \ No newline at end of file + val children: List +) \ No newline at end of file diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/db/User.kt b/app/src/main/java/ru/risdeveau/pixeldragon/db/User.kt index 2e0e102..ca5f78a 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/db/User.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/db/User.kt @@ -1,18 +1,59 @@ /* - * Created by sweetbread on 22.02.2025, 15:45 + * Created by sweetbread * Copyright (c) 2025. All rights reserved. - * Last modified 21.02.2025, 13:38 */ package ru.risdeveau.pixeldragon.db -import androidx.room.ColumnInfo +import androidx.room.Dao +import androidx.room.Delete import androidx.room.Entity +import androidx.room.Insert 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 -data class User( - @PrimaryKey val uid: String, - @ColumnInfo(name = "name") val name: String?, - @ColumnInfo(name = "avatar") val avatar: String? -) \ No newline at end of file +@Entity(tableName = "user") +@TypeConverters(Converters::class) +data class UserDB ( + @PrimaryKey val id: 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) +} \ No newline at end of file diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/repo/Room.kt b/app/src/main/java/ru/risdeveau/pixeldragon/repo/Room.kt new file mode 100755 index 0000000..471712b --- /dev/null +++ b/app/src/main/java/ru/risdeveau/pixeldragon/repo/Room.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/repo/User.kt b/app/src/main/java/ru/risdeveau/pixeldragon/repo/User.kt new file mode 100755 index 0000000..24a2c10 --- /dev/null +++ b/app/src/main/java/ru/risdeveau/pixeldragon/repo/User.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/Rooms.kt b/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/Rooms.kt index c7bfc4a..fc5bbc8 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/Rooms.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/Rooms.kt @@ -35,11 +35,8 @@ import androidx.navigation.NavController import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch 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.getUserProfile -import ru.risdeveau.pixeldragon.db.Room +import ru.risdeveau.pixeldragon.repo.Room import ru.risdeveau.pixeldragon.ui.item.MXCImage @Composable @@ -79,31 +76,19 @@ fun RoomList(modifier: Modifier = Modifier, navController: NavController) { @Composable fun RoomItem(modifier: Modifier = Modifier, rid: String, navController: NavController) { var room by remember { mutableStateOf(null) } - var directUser by remember { mutableStateOf(null) } val scope = rememberCoroutineScope() LaunchedEffect(true) { scope.launch { withContext(Dispatchers.IO) { - room = getRoom(rid) - } - } - } - - LaunchedEffect(room?.direct) { - room?.let { - it.direct?.let { uid -> - scope.launch { - withContext(Dispatchers.IO) { - directUser = getUserProfile(uid) - } - } + room = Room.getById(rid) } } } if (room != null) { - val avatarUrl = room!!.avatarUrl ?: (directUser?.avatarUrl ?: "") + val room = room!! + val avatarUrl = room.avatarUrl ?: (room.direct?.avatarUrl ?: "") Row( modifier @@ -112,14 +97,14 @@ fun RoomItem(modifier: Modifier = Modifier, rid: String, navController: NavContr .fillMaxWidth() .background( color = - if (room!!.type == "m.space") + if (room.type == "m.space") MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.background ) .clip(RoundedCornerShape(12.dp)) .clickable { - if (room!!.type == "m.space") + if (room.type == "m.space") navController.navigate("space/$rid") else navController.navigate("room/$rid") @@ -133,16 +118,16 @@ fun RoomItem(modifier: Modifier = Modifier, rid: String, navController: NavContr .height(52.dp) .width(52.dp) .let { - if (room!!.type == "m.space") + if (room.type == "m.space") it.clip(RoundedCornerShape(12.dp)) else it.clip(CircleShape) } ) Column { - Text(room!!.type) + Text(room.type) Text( - room!!.name ?: "Unnamed", + room.name ?: "Unnamed", maxLines = 1, color = MaterialTheme.colorScheme.primary, fontSize = MaterialTheme.typography.titleLarge.fontSize