From 4f0845bf46731c63d7be6648716dfab576c70930 Mon Sep 17 00:00:00 2001 From: Infi Date: Sun, 18 Jun 2023 03:54:13 +0200 Subject: [PATCH] feat: basic member fetching in servers, roles in sheet Signed-off-by: Infi --- .../main/java/chat/revolt/api/RevoltAPI.kt | 1 - .../java/chat/revolt/api/internals/Members.kt | 23 ++++-- .../chat/revolt/api/routes/channel/Channel.kt | 6 +- .../chat/revolt/api/routes/server/Server.kt | 74 ++++++++++++++++++- .../revolt/screens/chat/ChatRouterScreen.kt | 17 +++-- .../views/channel/ChannelScreenViewModel.kt | 17 ++++- .../chat/revolt/sheets/UserContextSheet.kt | 54 ++++++++++++-- app/src/main/res/values/strings.xml | 1 + 8 files changed, 169 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/chat/revolt/api/RevoltAPI.kt b/app/src/main/java/chat/revolt/api/RevoltAPI.kt index 9a0e195a..694a1fd9 100644 --- a/app/src/main/java/chat/revolt/api/RevoltAPI.kt +++ b/app/src/main/java/chat/revolt/api/RevoltAPI.kt @@ -92,7 +92,6 @@ val mainHandler = Handler(Looper.getMainLooper()) object RevoltAPI { const val TOKEN_HEADER_NAME = "x-session-token" - // FIXME discount caching solutions! LRU would be better but this is fine for now val userCache = mutableStateMapOf() val serverCache = mutableStateMapOf() val channelCache = mutableStateMapOf() diff --git a/app/src/main/java/chat/revolt/api/internals/Members.kt b/app/src/main/java/chat/revolt/api/internals/Members.kt index a203889d..2aa7c0bc 100644 --- a/app/src/main/java/chat/revolt/api/internals/Members.kt +++ b/app/src/main/java/chat/revolt/api/internals/Members.kt @@ -2,14 +2,23 @@ package chat.revolt.api.internals import chat.revolt.api.schemas.Member -@RequiresOptIn("Dummy API, does nothing or returns null.") -@Retention(AnnotationRetention.BINARY) -@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) -annotation class RvxDummyMemberAPI - object Members { - @RvxDummyMemberAPI + // memberCache (mapping of serverId to userId to member) + private val memberCache = mutableMapOf>() + fun getMember(serverId: String, userId: String): Member? { - return null + return memberCache[serverId]?.get(userId) + } + + fun hasMember(serverId: String, userId: String): Boolean { + return memberCache[serverId]?.containsKey(userId) ?: false + } + + fun addMember(serverId: String, member: Member) { + if (!memberCache.containsKey(serverId)) { + memberCache[serverId] = mutableMapOf() + } + + memberCache[serverId]?.set(member.id.user, member) } } \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/routes/channel/Channel.kt b/app/src/main/java/chat/revolt/api/routes/channel/Channel.kt index a2086432..a86e3ba6 100644 --- a/app/src/main/java/chat/revolt/api/routes/channel/Channel.kt +++ b/app/src/main/java/chat/revolt/api/routes/channel/Channel.kt @@ -23,7 +23,7 @@ import kotlinx.serialization.builtins.ListSerializer suspend fun fetchMessagesFromChannel( channelId: String, limit: Int = 50, - include_users: Boolean = false, + includeUsers: Boolean = false, before: String? = null, after: String? = null, nearby: String? = null, @@ -33,7 +33,7 @@ suspend fun fetchMessagesFromChannel( headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken) parameter("limit", limit) - parameter("include_users", include_users) + parameter("include_users", includeUsers) if (before != null) parameter("before", before) if (after != null) parameter("after", after) @@ -42,7 +42,7 @@ suspend fun fetchMessagesFromChannel( } .bodyAsText() - if (include_users) { + if (includeUsers) { return RevoltJson.decodeFromString( MessagesInChannel.serializer(), response diff --git a/app/src/main/java/chat/revolt/api/routes/server/Server.kt b/app/src/main/java/chat/revolt/api/routes/server/Server.kt index 913dd73e..3d245923 100644 --- a/app/src/main/java/chat/revolt/api/routes/server/Server.kt +++ b/app/src/main/java/chat/revolt/api/routes/server/Server.kt @@ -1,11 +1,83 @@ package chat.revolt.api.routes.server import chat.revolt.api.RevoltAPI +import chat.revolt.api.RevoltError import chat.revolt.api.RevoltHttp -import io.ktor.client.request.* +import chat.revolt.api.RevoltJson +import chat.revolt.api.internals.Members +import chat.revolt.api.schemas.Member +import chat.revolt.api.schemas.User +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import io.ktor.client.request.put +import io.ktor.client.statement.bodyAsText +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException + +@Serializable +data class FetchMembersResponse( + val members: List, + val users: List +) suspend fun ackServer(serverId: String) { RevoltHttp.put("/servers/$serverId/ack") { headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken) } +} + +suspend fun fetchMembers( + serverId: String, + includeOffline: Boolean = false, + pure: Boolean = false +): FetchMembersResponse { + val response = RevoltHttp.get("/servers/$serverId/members") { + headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken) + + parameter("exclude_offline", !includeOffline) + } + + val responseContent = response.bodyAsText() + + try { + val error = RevoltJson.decodeFromString(RevoltError.serializer(), responseContent) + throw Error(error.type) + } catch (e: SerializationException) { + // Not an error + } + + val membersResponse = + RevoltJson.decodeFromString(FetchMembersResponse.serializer(), responseContent) + + if (pure) { + return membersResponse + } + + membersResponse.members.forEach { member -> + if (!Members.hasMember(serverId, member.id.user)) { + Members.addMember(serverId, member) + } + } + + membersResponse.users.forEach { user -> + user.id?.let { RevoltAPI.userCache.putIfAbsent(it, user) } + } + + return membersResponse +} + +suspend fun fetchMember(serverId: String, userId: String, pure: Boolean = false): Member { + val response = RevoltHttp.get("/servers/$serverId/members/$userId") { + headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken) + } + + val member = RevoltJson.decodeFromString(Member.serializer(), response.bodyAsText()) + + if (!pure) { + if (!Members.hasMember(serverId, member.id.user)) { + Members.addMember(serverId, member) + } + } + + return member } \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt index c947e524..dcbe27cb 100644 --- a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt @@ -54,6 +54,7 @@ import chat.revolt.R import chat.revolt.api.RevoltAPI import chat.revolt.api.realtime.DisconnectionState import chat.revolt.api.realtime.RealtimeSocket +import chat.revolt.api.routes.server.fetchMembers import chat.revolt.api.schemas.User import chat.revolt.api.settings.SyncedSettings import chat.revolt.components.chat.DisconnectedNotice @@ -101,12 +102,12 @@ class ChatRouterViewModel @Inject constructor( } } - private fun setCurrentServer(serverId: String, save: Boolean = true) { + private suspend fun setCurrentServer(serverId: String, save: Boolean = true) { currentServer = serverId - if (save) viewModelScope.launch { - kvStorage.set("currentServer", serverId) - } + if (save) kvStorage.set("currentServer", serverId) + + if (serverId != "home") fetchMembers(serverId, includeOffline = false, pure = false) } private fun setSaveCurrentChannel(channelId: String) { @@ -129,13 +130,17 @@ class ChatRouterViewModel @Inject constructor( popUpTo(route) } } - setCurrentServer("home") + viewModelScope.launch { + setCurrentServer("home") + } return } val channelId = RevoltAPI.serverCache[serverId]?.channels?.firstOrNull() - setCurrentServer(serverId, channelId != null) + viewModelScope.launch { + setCurrentServer(serverId, channelId != null) + } if (channelId != null) { navigateToChannel(channelId, navController) diff --git a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt index f3e5ee57..74b0520a 100644 --- a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt +++ b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt @@ -11,6 +11,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltJson +import chat.revolt.api.internals.Members import chat.revolt.api.internals.ULID import chat.revolt.api.realtime.RealtimeSocketFrames import chat.revolt.api.realtime.frames.receivable.ChannelStartTypingFrame @@ -27,6 +28,7 @@ import chat.revolt.api.routes.channel.sendMessage import chat.revolt.api.routes.microservices.autumn.FileArgs import chat.revolt.api.routes.microservices.autumn.MAX_ATTACHMENTS_PER_MESSAGE import chat.revolt.api.routes.microservices.autumn.uploadToAutumn +import chat.revolt.api.routes.server.fetchMember import chat.revolt.api.routes.user.addUserIfUnknown import chat.revolt.api.schemas.Channel import chat.revolt.api.schemas.Message @@ -101,7 +103,7 @@ class ChannelScreenViewModel : ViewModel() { fetchMessagesFromChannel( activeChannel!!.id!!, limit = 50, - true, + includeUsers = true, before = if (renderableMessages.isNotEmpty()) { renderableMessages.last().id } else { @@ -119,6 +121,18 @@ class ChannelScreenViewModel : ViewModel() { } messages.add(message) } + + it.users?.forEach { user -> + if (!RevoltAPI.userCache.containsKey(user.id)) { + RevoltAPI.userCache[user.id!!] = user + } + } + + it.members?.forEach { member -> + if (!Members.hasMember(member.id.server, member.id.user)) { + Members.addMember(member.id.server, member) + } + } } regroupMessages(renderableMessages + messages) @@ -260,6 +274,7 @@ class ChannelScreenViewModel : ViewModel() { if (it.channel != activeChannel?.id) return@onEach addUserIfUnknown(it.author!!) + activeChannel?.server?.let { s -> fetchMember(s, it.author) } regroupMessages(listOf(it) + renderableMessages) ackNewest() } diff --git a/app/src/main/java/chat/revolt/sheets/UserContextSheet.kt b/app/src/main/java/chat/revolt/sheets/UserContextSheet.kt index a8b2ff96..f8266bb1 100644 --- a/app/src/main/java/chat/revolt/sheets/UserContextSheet.kt +++ b/app/src/main/java/chat/revolt/sheets/UserContextSheet.kt @@ -1,12 +1,19 @@ package chat.revolt.sheets +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -17,17 +24,20 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import chat.revolt.R import chat.revolt.api.RevoltAPI import chat.revolt.api.internals.Members -import chat.revolt.api.internals.RvxDummyMemberAPI +import chat.revolt.api.internals.WebCompat +import chat.revolt.api.internals.solidColor import chat.revolt.api.routes.user.fetchUserProfile import chat.revolt.api.schemas.Profile import chat.revolt.components.generic.UIMarkdown import chat.revolt.components.screens.settings.RawUserOverview +@OptIn(ExperimentalLayoutApi::class) @Composable fun UserContextSheet( userId: String, @@ -36,7 +46,6 @@ fun UserContextSheet( ) { val user = RevoltAPI.userCache[userId] - @OptIn(RvxDummyMemberAPI::class) val member = serverId?.let { Members.getMember(it, userId) } val server = RevoltAPI.serverCache[serverId] @@ -66,9 +75,44 @@ fun UserContextSheet( Column( modifier = Modifier.padding(16.dp) ) { - Text( - text = "sheet for ${server?.name ?: "serverless (omg jamstack reference??)"}", - ) + member?.roles?.let { + Text( + text = stringResource(id = R.string.user_context_sheet_category_roles), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(bottom = 10.dp) + ) + + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + it.forEach { roleId -> + val role = server?.roles?.get(roleId) + role?.let { + Box( + modifier = Modifier + .border( + border = BorderStroke( + width = 1.dp, + brush = role.colour?.let { WebCompat.parseColour(it) } + ?: Brush.solidColor(LocalContentColor.current), + ), + shape = MaterialTheme.shapes.small + ) + .padding(8.dp) + ) { + Text( + text = role.name ?: roleId, + style = LocalTextStyle.current.copy( + brush = role.colour?.let { WebCompat.parseColour(it) } + ?: Brush.solidColor(LocalContentColor.current) + ) + ) + } + } + } + } + } Text( text = stringResource(id = R.string.user_context_sheet_category_bio), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8645da4d..9322bf81 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -181,6 +181,7 @@ Bio This user hasn\'t set a bio yet. + Roles Add a server Join by invite code or link