feat: basic member fetching in servers, roles in sheet

Signed-off-by: Infi <wingit@geist.ga>
This commit is contained in:
Infi 2023-06-18 03:54:13 +02:00
parent 6e179d3b49
commit 4f0845bf46
8 changed files with 169 additions and 24 deletions

View File

@ -92,7 +92,6 @@ val mainHandler = Handler(Looper.getMainLooper())
object RevoltAPI { object RevoltAPI {
const val TOKEN_HEADER_NAME = "x-session-token" 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<String, User>() val userCache = mutableStateMapOf<String, User>()
val serverCache = mutableStateMapOf<String, Server>() val serverCache = mutableStateMapOf<String, Server>()
val channelCache = mutableStateMapOf<String, ChannelSchema>() val channelCache = mutableStateMapOf<String, ChannelSchema>()

View File

@ -2,14 +2,23 @@ package chat.revolt.api.internals
import chat.revolt.api.schemas.Member 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 { object Members {
@RvxDummyMemberAPI // memberCache (mapping of serverId to userId to member)
private val memberCache = mutableMapOf<String, MutableMap<String, Member>>()
fun getMember(serverId: String, userId: String): Member? { 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)
} }
} }

View File

@ -23,7 +23,7 @@ import kotlinx.serialization.builtins.ListSerializer
suspend fun fetchMessagesFromChannel( suspend fun fetchMessagesFromChannel(
channelId: String, channelId: String,
limit: Int = 50, limit: Int = 50,
include_users: Boolean = false, includeUsers: Boolean = false,
before: String? = null, before: String? = null,
after: String? = null, after: String? = null,
nearby: String? = null, nearby: String? = null,
@ -33,7 +33,7 @@ suspend fun fetchMessagesFromChannel(
headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken) headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)
parameter("limit", limit) parameter("limit", limit)
parameter("include_users", include_users) parameter("include_users", includeUsers)
if (before != null) parameter("before", before) if (before != null) parameter("before", before)
if (after != null) parameter("after", after) if (after != null) parameter("after", after)
@ -42,7 +42,7 @@ suspend fun fetchMessagesFromChannel(
} }
.bodyAsText() .bodyAsText()
if (include_users) { if (includeUsers) {
return RevoltJson.decodeFromString( return RevoltJson.decodeFromString(
MessagesInChannel.serializer(), MessagesInChannel.serializer(),
response response

View File

@ -1,11 +1,83 @@
package chat.revolt.api.routes.server package chat.revolt.api.routes.server
import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltAPI
import chat.revolt.api.RevoltError
import chat.revolt.api.RevoltHttp 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<Member>,
val users: List<User>
)
suspend fun ackServer(serverId: String) { suspend fun ackServer(serverId: String) {
RevoltHttp.put("/servers/$serverId/ack") { RevoltHttp.put("/servers/$serverId/ack") {
headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken) 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
}

View File

@ -54,6 +54,7 @@ import chat.revolt.R
import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltAPI
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.server.fetchMembers
import chat.revolt.api.schemas.User import chat.revolt.api.schemas.User
import chat.revolt.api.settings.SyncedSettings import chat.revolt.api.settings.SyncedSettings
import chat.revolt.components.chat.DisconnectedNotice 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 currentServer = serverId
if (save) viewModelScope.launch { if (save) kvStorage.set("currentServer", serverId)
kvStorage.set("currentServer", serverId)
} if (serverId != "home") fetchMembers(serverId, includeOffline = false, pure = false)
} }
private fun setSaveCurrentChannel(channelId: String) { private fun setSaveCurrentChannel(channelId: String) {
@ -129,13 +130,17 @@ class ChatRouterViewModel @Inject constructor(
popUpTo(route) popUpTo(route)
} }
} }
setCurrentServer("home") viewModelScope.launch {
setCurrentServer("home")
}
return return
} }
val channelId = RevoltAPI.serverCache[serverId]?.channels?.firstOrNull() val channelId = RevoltAPI.serverCache[serverId]?.channels?.firstOrNull()
setCurrentServer(serverId, channelId != null) viewModelScope.launch {
setCurrentServer(serverId, channelId != null)
}
if (channelId != null) { if (channelId != null) {
navigateToChannel(channelId, navController) navigateToChannel(channelId, navController)

View File

@ -11,6 +11,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltAPI
import chat.revolt.api.RevoltJson import chat.revolt.api.RevoltJson
import chat.revolt.api.internals.Members
import chat.revolt.api.internals.ULID import chat.revolt.api.internals.ULID
import chat.revolt.api.realtime.RealtimeSocketFrames import chat.revolt.api.realtime.RealtimeSocketFrames
import chat.revolt.api.realtime.frames.receivable.ChannelStartTypingFrame 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.FileArgs
import chat.revolt.api.routes.microservices.autumn.MAX_ATTACHMENTS_PER_MESSAGE import chat.revolt.api.routes.microservices.autumn.MAX_ATTACHMENTS_PER_MESSAGE
import chat.revolt.api.routes.microservices.autumn.uploadToAutumn 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.routes.user.addUserIfUnknown
import chat.revolt.api.schemas.Channel import chat.revolt.api.schemas.Channel
import chat.revolt.api.schemas.Message import chat.revolt.api.schemas.Message
@ -101,7 +103,7 @@ class ChannelScreenViewModel : ViewModel() {
fetchMessagesFromChannel( fetchMessagesFromChannel(
activeChannel!!.id!!, activeChannel!!.id!!,
limit = 50, limit = 50,
true, includeUsers = true,
before = if (renderableMessages.isNotEmpty()) { before = if (renderableMessages.isNotEmpty()) {
renderableMessages.last().id renderableMessages.last().id
} else { } else {
@ -119,6 +121,18 @@ class ChannelScreenViewModel : ViewModel() {
} }
messages.add(message) 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) regroupMessages(renderableMessages + messages)
@ -260,6 +274,7 @@ class ChannelScreenViewModel : ViewModel() {
if (it.channel != activeChannel?.id) return@onEach if (it.channel != activeChannel?.id) return@onEach
addUserIfUnknown(it.author!!) addUserIfUnknown(it.author!!)
activeChannel?.server?.let { s -> fetchMember(s, it.author) }
regroupMessages(listOf(it) + renderableMessages) regroupMessages(listOf(it) + renderableMessages)
ackNewest() ackNewest()
} }

View File

@ -1,12 +1,19 @@
package chat.revolt.sheets 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.Box
import androidx.compose.foundation.layout.Column 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.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
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
@ -17,17 +24,20 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.revolt.R import chat.revolt.R
import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltAPI
import chat.revolt.api.internals.Members 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.routes.user.fetchUserProfile
import chat.revolt.api.schemas.Profile import chat.revolt.api.schemas.Profile
import chat.revolt.components.generic.UIMarkdown import chat.revolt.components.generic.UIMarkdown
import chat.revolt.components.screens.settings.RawUserOverview import chat.revolt.components.screens.settings.RawUserOverview
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun UserContextSheet( fun UserContextSheet(
userId: String, userId: String,
@ -36,7 +46,6 @@ fun UserContextSheet(
) { ) {
val user = RevoltAPI.userCache[userId] val user = RevoltAPI.userCache[userId]
@OptIn(RvxDummyMemberAPI::class)
val member = serverId?.let { Members.getMember(it, userId) } val member = serverId?.let { Members.getMember(it, userId) }
val server = RevoltAPI.serverCache[serverId] val server = RevoltAPI.serverCache[serverId]
@ -66,9 +75,44 @@ fun UserContextSheet(
Column( Column(
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp)
) { ) {
Text( member?.roles?.let {
text = "sheet for ${server?.name ?: "serverless (omg jamstack reference??)"}", 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(
text = stringResource(id = R.string.user_context_sheet_category_bio), text = stringResource(id = R.string.user_context_sheet_category_bio),

View File

@ -181,6 +181,7 @@
<string name="user_context_sheet_category_bio">Bio</string> <string name="user_context_sheet_category_bio">Bio</string>
<string name="user_context_sheet_bio_empty">This user hasn\'t set a bio yet.</string> <string name="user_context_sheet_bio_empty">This user hasn\'t set a bio yet.</string>
<string name="user_context_sheet_category_roles">Roles</string>
<string name="add_server_sheet_title">Add a server</string> <string name="add_server_sheet_title">Add a server</string>
<string name="add_server_sheet_join_by_invite">Join by invite code or link</string> <string name="add_server_sheet_join_by_invite">Join by invite code or link</string>