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 {
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 serverCache = mutableStateMapOf<String, Server>()
val channelCache = mutableStateMapOf<String, ChannelSchema>()

View File

@ -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<String, MutableMap<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(
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

View File

@ -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<Member>,
val users: List<User>
)
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
}

View File

@ -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)

View File

@ -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()
}

View File

@ -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),

View File

@ -181,6 +181,7 @@
<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_category_roles">Roles</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>