feat: cache servers and channels locally
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
ae335c1226
commit
f48bda178a
|
|
@ -4,3 +4,5 @@
|
||||||
/kotlinc.xml
|
/kotlinc.xml
|
||||||
/appInsightsSettings.xml
|
/appInsightsSettings.xml
|
||||||
/other.xml
|
/other.xml
|
||||||
|
# GitHub Copilot persisted chat sessions
|
||||||
|
/copilot/chatSessions
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ plugins {
|
||||||
id 'com.google.devtools.ksp'
|
id 'com.google.devtools.ksp'
|
||||||
id 'org.jmailen.kotlinter'
|
id 'org.jmailen.kotlinter'
|
||||||
id "io.sentry.android.gradle" version "3.4.2"
|
id "io.sentry.android.gradle" version "3.4.2"
|
||||||
|
id "app.cash.sqldelight" version "2.0.1"
|
||||||
|
|
||||||
id 'kotlin-kapt'
|
id 'kotlin-kapt'
|
||||||
id 'kotlin-parcelize'
|
id 'kotlin-parcelize'
|
||||||
|
|
@ -213,7 +214,7 @@ dependencies {
|
||||||
|
|
||||||
// Glide - Image Loading
|
// Glide - Image Loading
|
||||||
implementation "com.github.bumptech.glide:glide:$glide_version"
|
implementation "com.github.bumptech.glide:glide:$glide_version"
|
||||||
implementation "com.github.bumptech.glide:compose:1.0.0-beta1-SNAPSHOT"
|
implementation "com.github.bumptech.glide:compose:1.0.0-beta01"
|
||||||
ksp "com.github.bumptech.glide:ksp:$glide_version"
|
ksp "com.github.bumptech.glide:ksp:$glide_version"
|
||||||
|
|
||||||
// AboutLibraries - automated OSS library attribution
|
// AboutLibraries - automated OSS library attribution
|
||||||
|
|
@ -230,8 +231,6 @@ dependencies {
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation "androidx.browser:browser:1.7.0"
|
implementation "androidx.browser:browser:1.7.0"
|
||||||
implementation "androidx.webkit:webkit:1.9.0"
|
implementation "androidx.webkit:webkit:1.9.0"
|
||||||
implementation "androidx.datastore:datastore-preferences:1.1.0-alpha07"
|
|
||||||
implementation "androidx.datastore:datastore:1.1.0-alpha07"
|
|
||||||
implementation "androidx.core:core-splashscreen:1.0.1"
|
implementation "androidx.core:core-splashscreen:1.0.1"
|
||||||
|
|
||||||
// Libraries used for legacy View-based UI
|
// Libraries used for legacy View-based UI
|
||||||
|
|
@ -255,4 +254,17 @@ dependencies {
|
||||||
implementation "com.github.skydoves:colorpicker-compose:1.0.5"
|
implementation "com.github.skydoves:colorpicker-compose:1.0.5"
|
||||||
implementation "me.saket.telephoto:zoomable-image:1.0.0-alpha02"
|
implementation "me.saket.telephoto:zoomable-image:1.0.0-alpha02"
|
||||||
implementation "me.saket.telephoto:zoomable-image-glide:1.0.0-alpha02"
|
implementation "me.saket.telephoto:zoomable-image-glide:1.0.0-alpha02"
|
||||||
|
|
||||||
|
// Persistence
|
||||||
|
implementation "app.cash.sqldelight:android-driver:2.0.1"
|
||||||
|
implementation "androidx.datastore:datastore:1.1.0-beta02"
|
||||||
|
implementation "androidx.datastore:datastore-preferences:1.1.0-beta02"
|
||||||
|
}
|
||||||
|
|
||||||
|
sqldelight {
|
||||||
|
databases {
|
||||||
|
Database {
|
||||||
|
packageName = "chat.revolt.persistence"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -244,6 +244,8 @@ class MainActivity : FragmentActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RevoltAPI.hydrateFromPersistentCache()
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val windowSizeClass = calculateWindowSizeClass(this)
|
val windowSizeClass = calculateWindowSizeClass(this)
|
||||||
AppEntrypoint(
|
AppEntrypoint(
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,15 @@ import chat.revolt.api.internals.Members
|
||||||
import chat.revolt.api.realtime.DisconnectionState
|
import chat.revolt.api.realtime.DisconnectionState
|
||||||
import chat.revolt.api.realtime.RealtimeSocket
|
import chat.revolt.api.realtime.RealtimeSocket
|
||||||
import chat.revolt.api.routes.user.fetchSelf
|
import chat.revolt.api.routes.user.fetchSelf
|
||||||
|
import chat.revolt.api.schemas.AutumnResource
|
||||||
|
import chat.revolt.api.schemas.ChannelType
|
||||||
import chat.revolt.api.schemas.Emoji
|
import chat.revolt.api.schemas.Emoji
|
||||||
import chat.revolt.api.schemas.Message
|
import chat.revolt.api.schemas.Message
|
||||||
import chat.revolt.api.schemas.Server
|
import chat.revolt.api.schemas.Server
|
||||||
import chat.revolt.api.schemas.User
|
import chat.revolt.api.schemas.User
|
||||||
import chat.revolt.api.unreads.Unreads
|
import chat.revolt.api.unreads.Unreads
|
||||||
|
import chat.revolt.persistence.Database
|
||||||
|
import chat.revolt.persistence.SqlStorage
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.engine.okhttp.OkHttp
|
import io.ktor.client.engine.okhttp.OkHttp
|
||||||
import io.ktor.client.plugins.DefaultRequest
|
import io.ktor.client.plugins.DefaultRequest
|
||||||
|
|
@ -25,6 +29,7 @@ import io.ktor.client.plugins.logging.Logging
|
||||||
import io.ktor.client.plugins.websocket.WebSockets
|
import io.ktor.client.plugins.websocket.WebSockets
|
||||||
import io.ktor.client.request.header
|
import io.ktor.client.request.header
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
|
import io.sentry.Sentry
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
@ -138,6 +143,8 @@ object RevoltAPI {
|
||||||
|
|
||||||
private var socketCoroutine: Job? = null
|
private var socketCoroutine: Job? = null
|
||||||
|
|
||||||
|
private var openForLocalHydration = true
|
||||||
|
|
||||||
fun setSessionHeader(token: String) {
|
fun setSessionHeader(token: String) {
|
||||||
sessionToken = token
|
sessionToken = token
|
||||||
}
|
}
|
||||||
|
|
@ -231,6 +238,8 @@ object RevoltAPI {
|
||||||
|
|
||||||
socketCoroutine?.cancel()
|
socketCoroutine?.cancel()
|
||||||
mainHandler.removeCallbacksAndMessages(null)
|
mainHandler.removeCallbacksAndMessages(null)
|
||||||
|
|
||||||
|
clearPersistentCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -245,6 +254,87 @@ object RevoltAPI {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hydrate caches from a local database.
|
||||||
|
*/
|
||||||
|
fun hydrateFromPersistentCache() {
|
||||||
|
if (!openForLocalHydration) {
|
||||||
|
Log.w("RevoltAPI", "Hydration is closed, but was called")
|
||||||
|
// Stale data is worst case, let's track it even in prod
|
||||||
|
Sentry.captureMessage("Local hydration called twice or after real data was fetched")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val db = Database(SqlStorage.driver)
|
||||||
|
|
||||||
|
val channels = db.channelQueries.selectAll().executeAsList().map {
|
||||||
|
ChannelSchema(
|
||||||
|
id = it.id,
|
||||||
|
channelType = try {
|
||||||
|
ChannelType.valueOf(it.channelType)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
user = it.userId,
|
||||||
|
name = it.name,
|
||||||
|
owner = it.owner,
|
||||||
|
description = it.description,
|
||||||
|
recipients = selfId?.let { selfId ->
|
||||||
|
it.userId?.let { u -> listOf(u, selfId) }
|
||||||
|
} ?: it.userId?.let { u -> listOf(u) },
|
||||||
|
icon = AutumnResource(
|
||||||
|
id = it.iconId,
|
||||||
|
),
|
||||||
|
server = it.server,
|
||||||
|
lastMessageID = it.lastMessageId,
|
||||||
|
active = it.active == 1L,
|
||||||
|
nsfw = it.nsfw == 1L
|
||||||
|
)
|
||||||
|
}
|
||||||
|
channelCache.clear()
|
||||||
|
channelCache.putAll(channels.associateBy { it.id!! })
|
||||||
|
|
||||||
|
val servers = db.serverQueries.selectAll().executeAsList().map {
|
||||||
|
Server(
|
||||||
|
id = it.id,
|
||||||
|
owner = it.owner,
|
||||||
|
name = it.name,
|
||||||
|
description = it.description,
|
||||||
|
icon = AutumnResource(
|
||||||
|
id = it.iconId,
|
||||||
|
),
|
||||||
|
banner = AutumnResource(
|
||||||
|
id = it.bannerId,
|
||||||
|
),
|
||||||
|
flags = it.flags,
|
||||||
|
channels = channels
|
||||||
|
.filter { c -> c.server == it.id }
|
||||||
|
.filterNot { c -> c.id == null }
|
||||||
|
.map { c -> c.id!! },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
serverCache.clear()
|
||||||
|
serverCache.putAll(servers.associateBy { it.id!! })
|
||||||
|
|
||||||
|
openForLocalHydration = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the local caching database.
|
||||||
|
*/
|
||||||
|
private fun clearPersistentCache() {
|
||||||
|
val db = Database(SqlStorage.driver)
|
||||||
|
db.serverQueries.clear()
|
||||||
|
db.channelQueries.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks database as hydrated (after real data was fetched, for example).
|
||||||
|
*/
|
||||||
|
fun closeHydration() {
|
||||||
|
openForLocalHydration = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ object Roles {
|
||||||
// lowest rank = highest role
|
// lowest rank = highest role
|
||||||
private fun highestRoleWithPredicate(roles: List<Role?>, predicate: (Role) -> Boolean): Role? {
|
private fun highestRoleWithPredicate(roles: List<Role?>, predicate: (Role) -> Boolean): Role? {
|
||||||
return roles.filter { role ->
|
return roles.filter { role ->
|
||||||
predicate(role!!)
|
if (role == null) return@filter false
|
||||||
|
predicate(role)
|
||||||
}.minByOrNull { role ->
|
}.minByOrNull { role ->
|
||||||
role?.rank ?: 0.0
|
role?.rank ?: 0.0
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +81,7 @@ object Roles {
|
||||||
|
|
||||||
ChannelType.TextChannel, ChannelType.VoiceChannel -> {
|
ChannelType.TextChannel, ChannelType.VoiceChannel -> {
|
||||||
val server = RevoltAPI.serverCache[channel.server]
|
val server = RevoltAPI.serverCache[channel.server]
|
||||||
// FIXME this is a stupid patch to prevent it from showing "no permission" on a channel on launch
|
// FIXME this is a stupid patch to prevent it from showing "no permission" on a channel on launch
|
||||||
?: return PermissionBit.GrantAllSafe.value
|
?: return PermissionBit.GrantAllSafe.value
|
||||||
|
|
||||||
if (server.owner == user?.id) return PermissionBit.GrantAllSafe.value
|
if (server.owner == user?.id) return PermissionBit.GrantAllSafe.value
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,11 @@ import chat.revolt.api.realtime.frames.sendable.AuthorizationFrame
|
||||||
import chat.revolt.api.realtime.frames.sendable.PingFrame
|
import chat.revolt.api.realtime.frames.sendable.PingFrame
|
||||||
import chat.revolt.api.routes.server.fetchMember
|
import chat.revolt.api.routes.server.fetchMember
|
||||||
import chat.revolt.api.schemas.Channel
|
import chat.revolt.api.schemas.Channel
|
||||||
|
import chat.revolt.api.schemas.ChannelType
|
||||||
import chat.revolt.api.settings.GlobalState
|
import chat.revolt.api.settings.GlobalState
|
||||||
import chat.revolt.api.settings.SyncedSettings
|
import chat.revolt.api.settings.SyncedSettings
|
||||||
|
import chat.revolt.persistence.Database
|
||||||
|
import chat.revolt.persistence.SqlStorage
|
||||||
import io.ktor.client.plugins.websocket.ws
|
import io.ktor.client.plugins.websocket.ws
|
||||||
import io.ktor.websocket.CloseReason
|
import io.ktor.websocket.CloseReason
|
||||||
import io.ktor.websocket.Frame
|
import io.ktor.websocket.Frame
|
||||||
|
|
@ -55,6 +58,7 @@ sealed class RealtimeSocketFrames {
|
||||||
}
|
}
|
||||||
|
|
||||||
object RealtimeSocket {
|
object RealtimeSocket {
|
||||||
|
val database = Database(SqlStorage.driver)
|
||||||
var socket: WebSocketSession? = null
|
var socket: WebSocketSession? = null
|
||||||
|
|
||||||
private var _disconnectionState = mutableStateOf(DisconnectionState.Reconnecting)
|
private var _disconnectionState = mutableStateOf(DisconnectionState.Reconnecting)
|
||||||
|
|
@ -148,13 +152,54 @@ object RealtimeSocket {
|
||||||
val serverMap = readyFrame.servers.associateBy { it.id!! }
|
val serverMap = readyFrame.servers.associateBy { it.id!! }
|
||||||
RevoltAPI.serverCache.putAll(serverMap)
|
RevoltAPI.serverCache.putAll(serverMap)
|
||||||
|
|
||||||
|
// Cache servers in persistent local database
|
||||||
|
readyFrame.servers.map {
|
||||||
|
if (it.id == null || it.owner == null || it.name == null) {
|
||||||
|
return@map
|
||||||
|
}
|
||||||
|
|
||||||
|
database.serverQueries.upsert(
|
||||||
|
it.id,
|
||||||
|
it.owner,
|
||||||
|
it.name,
|
||||||
|
it.description,
|
||||||
|
it.icon?.id,
|
||||||
|
it.banner?.id,
|
||||||
|
it.flags
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Log.d("RealtimeSocket", "Adding channels to cache.")
|
Log.d("RealtimeSocket", "Adding channels to cache.")
|
||||||
val channelMap = readyFrame.channels.associateBy { it.id!! }
|
val channelMap = readyFrame.channels.associateBy { it.id!! }
|
||||||
RevoltAPI.channelCache.putAll(channelMap)
|
RevoltAPI.channelCache.putAll(channelMap)
|
||||||
|
|
||||||
|
// Cache channels in persistent local database
|
||||||
|
readyFrame.channels.map {
|
||||||
|
if (it.id == null || it.name == null) {
|
||||||
|
return@map
|
||||||
|
}
|
||||||
|
|
||||||
|
database.channelQueries.upsert(
|
||||||
|
it.id,
|
||||||
|
it.channelType?.value ?: ChannelType.TextChannel.value,
|
||||||
|
it.user,
|
||||||
|
it.name,
|
||||||
|
it.owner,
|
||||||
|
it.description,
|
||||||
|
if (it.channelType == ChannelType.DirectMessage) it.recipients?.firstOrNull { u -> u != RevoltAPI.selfId } else null,
|
||||||
|
it.icon?.id,
|
||||||
|
it.lastMessageID,
|
||||||
|
if (it.active == true) 1L else 0L,
|
||||||
|
if (it.nsfw == true) 1L else 0L,
|
||||||
|
it.server
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Log.d("RealtimeSocket", "Adding emojis to cache.")
|
Log.d("RealtimeSocket", "Adding emojis to cache.")
|
||||||
val emojiMap = readyFrame.emojis.associateBy { it.id!! }
|
val emojiMap = readyFrame.emojis.associateBy { it.id!! }
|
||||||
RevoltAPI.emojiCache.putAll(emojiMap)
|
RevoltAPI.emojiCache.putAll(emojiMap)
|
||||||
|
|
||||||
|
RevoltAPI.closeHydration()
|
||||||
}
|
}
|
||||||
|
|
||||||
"Message" -> {
|
"Message" -> {
|
||||||
|
|
@ -388,8 +433,23 @@ object RealtimeSocket {
|
||||||
val existing = RevoltAPI.channelCache[channelUpdateFrame.id]
|
val existing = RevoltAPI.channelCache[channelUpdateFrame.id]
|
||||||
?: return // if we don't have the channel no point in updating it
|
?: return // if we don't have the channel no point in updating it
|
||||||
|
|
||||||
RevoltAPI.channelCache[channelUpdateFrame.id] =
|
val combined = existing.mergeWithPartial(channelUpdateFrame.data)
|
||||||
existing.mergeWithPartial(channelUpdateFrame.data)
|
RevoltAPI.channelCache[channelUpdateFrame.id] = combined
|
||||||
|
|
||||||
|
database.channelQueries.upsert(
|
||||||
|
channelUpdateFrame.id,
|
||||||
|
combined.channelType?.value ?: ChannelType.TextChannel.value,
|
||||||
|
combined.user,
|
||||||
|
combined.name,
|
||||||
|
combined.owner,
|
||||||
|
combined.description,
|
||||||
|
if (combined.channelType == ChannelType.DirectMessage) combined.recipients?.firstOrNull { u -> u != RevoltAPI.selfId } else null,
|
||||||
|
combined.icon?.id,
|
||||||
|
combined.lastMessageID,
|
||||||
|
if (combined.active == true) 1L else 0L,
|
||||||
|
if (combined.nsfw == true) 1L else 0L,
|
||||||
|
combined.server
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"ChannelCreate" -> {
|
"ChannelCreate" -> {
|
||||||
|
|
@ -402,6 +462,20 @@ object RealtimeSocket {
|
||||||
)
|
)
|
||||||
|
|
||||||
RevoltAPI.channelCache[channelCreateFrame.id!!] = channelCreateFrame
|
RevoltAPI.channelCache[channelCreateFrame.id!!] = channelCreateFrame
|
||||||
|
database.channelQueries.upsert(
|
||||||
|
channelCreateFrame.id,
|
||||||
|
channelCreateFrame.channelType?.value ?: ChannelType.TextChannel.value,
|
||||||
|
channelCreateFrame.user,
|
||||||
|
channelCreateFrame.name,
|
||||||
|
channelCreateFrame.owner,
|
||||||
|
channelCreateFrame.description,
|
||||||
|
if (channelCreateFrame.channelType == ChannelType.DirectMessage) channelCreateFrame.recipients?.firstOrNull { u -> u != RevoltAPI.selfId } else null,
|
||||||
|
channelCreateFrame.icon?.id,
|
||||||
|
channelCreateFrame.lastMessageID,
|
||||||
|
if (channelCreateFrame.active == true) 1L else 0L,
|
||||||
|
if (channelCreateFrame.nsfw == true) 1L else 0L,
|
||||||
|
channelCreateFrame.server
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"ChannelDelete" -> {
|
"ChannelDelete" -> {
|
||||||
|
|
@ -422,6 +496,7 @@ object RealtimeSocket {
|
||||||
}
|
}
|
||||||
|
|
||||||
RevoltAPI.channelCache.remove(channelDeleteFrame.id)
|
RevoltAPI.channelCache.remove(channelDeleteFrame.id)
|
||||||
|
database.channelQueries.delete(channelDeleteFrame.id)
|
||||||
|
|
||||||
if (currentChannel.server != null) {
|
if (currentChannel.server != null) {
|
||||||
val existingServer = RevoltAPI.serverCache[currentChannel.server]
|
val existingServer = RevoltAPI.serverCache[currentChannel.server]
|
||||||
|
|
@ -468,6 +543,18 @@ object RealtimeSocket {
|
||||||
if (channel.id == null) return@forEach
|
if (channel.id == null) return@forEach
|
||||||
RevoltAPI.channelCache[channel.id] = channel
|
RevoltAPI.channelCache[channel.id] = channel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (serverCreateFrame.server.owner != null && serverCreateFrame.server.name != null) {
|
||||||
|
database.serverQueries.upsert(
|
||||||
|
serverCreateFrame.id,
|
||||||
|
serverCreateFrame.server.owner,
|
||||||
|
serverCreateFrame.server.name,
|
||||||
|
serverCreateFrame.server.description,
|
||||||
|
serverCreateFrame.server.icon?.id,
|
||||||
|
serverCreateFrame.server.banner?.id,
|
||||||
|
serverCreateFrame.server.flags
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"ChannelStartTyping" -> {
|
"ChannelStartTyping" -> {
|
||||||
|
|
@ -516,6 +603,22 @@ object RealtimeSocket {
|
||||||
}
|
}
|
||||||
|
|
||||||
RevoltAPI.serverCache[serverUpdateFrame.id] = updated
|
RevoltAPI.serverCache[serverUpdateFrame.id] = updated
|
||||||
|
|
||||||
|
if (updated.id != null && updated.owner != null && updated.name != null) {
|
||||||
|
try {
|
||||||
|
database.serverQueries.upsert(
|
||||||
|
updated.id!!,
|
||||||
|
updated.owner!!,
|
||||||
|
updated.name!!,
|
||||||
|
updated.description,
|
||||||
|
updated.icon?.id,
|
||||||
|
updated.banner?.id,
|
||||||
|
updated.flags
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("RealtimeSocket", "Failed to update server in local database.")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"ServerDelete" -> {
|
"ServerDelete" -> {
|
||||||
|
|
@ -527,6 +630,7 @@ object RealtimeSocket {
|
||||||
)
|
)
|
||||||
|
|
||||||
RevoltAPI.serverCache.remove(serverDeleteFrame.id)
|
RevoltAPI.serverCache.remove(serverDeleteFrame.id)
|
||||||
|
database.serverQueries.delete(serverDeleteFrame.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
"ServerMemberUpdate" -> {
|
"ServerMemberUpdate" -> {
|
||||||
|
|
|
||||||
|
|
@ -158,9 +158,9 @@ fun GroupIcon(
|
||||||
.size(size),
|
.size(size),
|
||||||
contentAlignment = Alignment.BottomEnd
|
contentAlignment = Alignment.BottomEnd
|
||||||
) {
|
) {
|
||||||
if (icon != null) {
|
if (icon?.id != null) {
|
||||||
RemoteImage(
|
RemoteImage(
|
||||||
url = rawUrl ?: "$REVOLT_FILES/icons/${icon.id!!}/group.png",
|
url = rawUrl ?: "$REVOLT_FILES/icons/${icon.id}/group.png",
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
description = stringResource(id = R.string.avatar_alt, name),
|
description = stringResource(id = R.string.avatar_alt, name),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package chat.revolt.persistence
|
||||||
|
|
||||||
|
import app.cash.sqldelight.db.SqlDriver
|
||||||
|
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
|
||||||
|
import chat.revolt.RevoltApplication
|
||||||
|
|
||||||
|
object SqlStorage {
|
||||||
|
val driver: SqlDriver = AndroidSqliteDriver(
|
||||||
|
Database.Schema,
|
||||||
|
RevoltApplication.instance.applicationContext,
|
||||||
|
"revolt.db"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -4,16 +4,20 @@ import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material3.ElevatedButton
|
import androidx.compose.material3.ElevatedButton
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LargeTopAppBar
|
import androidx.compose.material3.LargeTopAppBar
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
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
|
||||||
|
|
@ -23,6 +27,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
|
@ -30,7 +35,9 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import chat.revolt.R
|
import chat.revolt.R
|
||||||
|
import chat.revolt.persistence.Database
|
||||||
import chat.revolt.persistence.KVStorage
|
import chat.revolt.persistence.KVStorage
|
||||||
|
import chat.revolt.persistence.SqlStorage
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -54,6 +61,9 @@ class DebugSettingsScreenViewModel @Inject constructor(
|
||||||
kvStorage.remove("latestChangelogRead")
|
kvStorage.remove("latestChangelogRead")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val serverQueries = Database(SqlStorage.driver).serverQueries.selectAll().executeAsList()
|
||||||
|
val channelQueries = Database(SqlStorage.driver).channelQueries.selectAll().executeAsList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
@ -128,6 +138,43 @@ fun DebugSettingsScreen(
|
||||||
Text("Mark latest changelog as unread")
|
Text("Mark latest changelog as unread")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Database",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
modifier = Modifier.padding(bottom = 10.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Servers: ${viewModel.serverQueries.size}",
|
||||||
|
modifier = Modifier.padding(bottom = 10.dp)
|
||||||
|
)
|
||||||
|
LazyColumn(modifier = Modifier.height(200.dp)) {
|
||||||
|
items(viewModel.serverQueries.size) { index ->
|
||||||
|
Text(
|
||||||
|
text = viewModel.serverQueries[index].toString(),
|
||||||
|
style = LocalTextStyle.current.copy(
|
||||||
|
fontFamily = FontFamily.Monospace
|
||||||
|
)
|
||||||
|
)
|
||||||
|
HorizontalDivider(Modifier.padding(vertical = 10.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = "Channels: ${viewModel.channelQueries.size}",
|
||||||
|
modifier = Modifier.padding(bottom = 10.dp)
|
||||||
|
)
|
||||||
|
LazyColumn(modifier = Modifier.height(200.dp)) {
|
||||||
|
items(viewModel.channelQueries.size) { index ->
|
||||||
|
Text(
|
||||||
|
text = viewModel.channelQueries[index].toString(),
|
||||||
|
style = LocalTextStyle.current.copy(
|
||||||
|
fontFamily = FontFamily.Monospace
|
||||||
|
)
|
||||||
|
)
|
||||||
|
HorizontalDivider(Modifier.padding(vertical = 10.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
CREATE TABLE Channel (
|
||||||
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
channelType TEXT NOT NULL,
|
||||||
|
userId TEXT,
|
||||||
|
name TEXT,
|
||||||
|
owner TEXT,
|
||||||
|
description TEXT,
|
||||||
|
dmPartner TEXT,
|
||||||
|
iconId TEXT,
|
||||||
|
lastMessageId TEXT,
|
||||||
|
active INTEGER,
|
||||||
|
nsfw INTEGER,
|
||||||
|
server TEXT,
|
||||||
|
FOREIGN KEY (server) REFERENCES Server(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_channel_server
|
||||||
|
ON Channel (server);
|
||||||
|
|
||||||
|
CREATE INDEX idx_channel_dmPartner
|
||||||
|
ON Channel (dmPartner);
|
||||||
|
|
||||||
|
selectAll:
|
||||||
|
SELECT *
|
||||||
|
FROM Channel;
|
||||||
|
|
||||||
|
upsert:
|
||||||
|
INSERT OR REPLACE
|
||||||
|
INTO Channel (id, channelType, userId, name, owner, description, dmPartner, iconId, lastMessageId, active, nsfw, server)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||||
|
|
||||||
|
clear:
|
||||||
|
DELETE
|
||||||
|
FROM Channel;
|
||||||
|
|
||||||
|
delete:
|
||||||
|
DELETE
|
||||||
|
FROM Channel
|
||||||
|
WHERE id = ?;
|
||||||
|
|
||||||
|
findDmByPartner:
|
||||||
|
SELECT *
|
||||||
|
FROM Channel
|
||||||
|
WHERE channelType = 'DirectMessage'
|
||||||
|
AND dmPartner = ?;
|
||||||
|
|
||||||
|
findByServer:
|
||||||
|
SELECT *
|
||||||
|
FROM Channel
|
||||||
|
WHERE server = ?;
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
CREATE TABLE Server (
|
||||||
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
owner TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
iconId TEXT,
|
||||||
|
bannerId TEXT,
|
||||||
|
flags INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_server_name
|
||||||
|
ON Server (name);
|
||||||
|
|
||||||
|
selectAll:
|
||||||
|
SELECT *
|
||||||
|
FROM Server;
|
||||||
|
|
||||||
|
upsert:
|
||||||
|
INSERT OR REPLACE
|
||||||
|
INTO Server (id, owner, name, description, iconId, bannerId, flags)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||||
|
|
||||||
|
clear:
|
||||||
|
DELETE
|
||||||
|
FROM Server;
|
||||||
|
|
||||||
|
delete:
|
||||||
|
DELETE
|
||||||
|
FROM Server
|
||||||
|
WHERE id = ?;
|
||||||
Loading…
Reference in New Issue