diff --git a/app/src/main/java/chat/revolt/api/RevoltAPI.kt b/app/src/main/java/chat/revolt/api/RevoltAPI.kt index 694a1fd9..6acec68c 100644 --- a/app/src/main/java/chat/revolt/api/RevoltAPI.kt +++ b/app/src/main/java/chat/revolt/api/RevoltAPI.kt @@ -5,6 +5,7 @@ import android.os.Looper import android.util.Log import androidx.compose.runtime.mutableStateMapOf import chat.revolt.BuildConfig +import chat.revolt.api.internals.Members import chat.revolt.api.realtime.DisconnectionState import chat.revolt.api.realtime.RealtimeSocket import chat.revolt.api.routes.user.fetchSelf @@ -98,6 +99,8 @@ object RevoltAPI { val emojiCache = mutableStateMapOf() val messageCache = mutableStateMapOf() + val members = Members() + val unreads = Unreads() var selfId: String? = null @@ -182,6 +185,7 @@ object RevoltAPI { emojiCache.clear() messageCache.clear() + members.clear() unreads.clear() socketCoroutine?.cancel() 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 2aa7c0bc..6fb6819b 100644 --- a/app/src/main/java/chat/revolt/api/internals/Members.kt +++ b/app/src/main/java/chat/revolt/api/internals/Members.kt @@ -2,7 +2,7 @@ package chat.revolt.api.internals import chat.revolt.api.schemas.Member -object Members { +class Members { // memberCache (mapping of serverId to userId to member) private val memberCache = mutableMapOf>() @@ -14,11 +14,19 @@ object Members { return memberCache[serverId]?.containsKey(userId) ?: false } - fun addMember(serverId: String, member: Member) { + fun setMember(serverId: String, member: Member) { if (!memberCache.containsKey(serverId)) { memberCache[serverId] = mutableMapOf() } - + memberCache[serverId]?.set(member.id.user, member) } + + fun removeMember(serverId: String, userId: String) { + memberCache[serverId]?.remove(userId) + } + + fun clear() { + memberCache.clear() + } } \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt b/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt index 1fa8f7e4..96f0235c 100644 --- a/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt +++ b/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt @@ -17,9 +17,15 @@ import chat.revolt.api.realtime.frames.receivable.MessageUpdateFrame import chat.revolt.api.realtime.frames.receivable.PongFrame import chat.revolt.api.realtime.frames.receivable.ReadyFrame import chat.revolt.api.realtime.frames.receivable.ServerCreateFrame +import chat.revolt.api.realtime.frames.receivable.ServerDeleteFrame +import chat.revolt.api.realtime.frames.receivable.ServerMemberJoinFrame +import chat.revolt.api.realtime.frames.receivable.ServerMemberLeaveFrame +import chat.revolt.api.realtime.frames.receivable.ServerMemberUpdateFrame +import chat.revolt.api.realtime.frames.receivable.ServerUpdateFrame import chat.revolt.api.realtime.frames.receivable.UserUpdateFrame import chat.revolt.api.realtime.frames.sendable.AuthorizationFrame import chat.revolt.api.realtime.frames.sendable.PingFrame +import chat.revolt.api.routes.server.fetchMember import io.ktor.client.plugins.websocket.ws import io.ktor.websocket.CloseReason import io.ktor.websocket.Frame @@ -288,6 +294,97 @@ object RealtimeSocket { RevoltAPI.wsFrameChannel.send(channelStopTypingFrame) } + "ServerUpdate" -> { + val serverUpdateFrame = + RevoltJson.decodeFromString(ServerUpdateFrame.serializer(), rawFrame) + Log.d( + "RealtimeSocket", + "Received server update frame for ${serverUpdateFrame.id}." + ) + + val existing = RevoltAPI.serverCache[serverUpdateFrame.id] + ?: return // if we don't have the server no point in updating it + + var updated = + existing.mergeWithPartial(serverUpdateFrame.data) + + serverUpdateFrame.clear?.forEach { + when (it) { + "Icon" -> updated = updated.copy(icon = null) + "Banner" -> updated = updated.copy(banner = null) + "Description" -> updated = updated.copy(description = null) + else -> Log.e("RealtimeSocket", "Unknown server clear field: $it") + } + } + + RevoltAPI.serverCache[serverUpdateFrame.id] = updated + } + + "ServerDelete" -> { + val serverDeleteFrame = + RevoltJson.decodeFromString(ServerDeleteFrame.serializer(), rawFrame) + Log.d( + "RealtimeSocket", + "Received server delete frame for ${serverDeleteFrame.id}." + ) + + RevoltAPI.serverCache.remove(serverDeleteFrame.id) + } + + "ServerMemberUpdate" -> { + val serverMemberUpdateFrame = + RevoltJson.decodeFromString(ServerMemberUpdateFrame.serializer(), rawFrame) + Log.d( + "RealtimeSocket", + "Received server member update frame for ${serverMemberUpdateFrame.id.user} in ${serverMemberUpdateFrame.id.server}." + ) + + val existing = RevoltAPI.members.getMember( + serverMemberUpdateFrame.id.server, + serverMemberUpdateFrame.id.user + ) + ?: return // if we don't have the member no point in updating them + + var updated = existing.mergeWithPartial(serverMemberUpdateFrame.data) + + serverMemberUpdateFrame.clear?.forEach { + when (it) { + "Avatar" -> updated = updated.copy(avatar = null) + "Nickname" -> updated = updated.copy(nickname = null) + else -> Log.e("RealtimeSocket", "Unknown server member clear field: $it") + } + } + + RevoltAPI.members.setMember(serverMemberUpdateFrame.id.server, updated) + } + + "ServerMemberJoin" -> { + val serverMemberJoinFrame = + RevoltJson.decodeFromString(ServerMemberJoinFrame.serializer(), rawFrame) + Log.d( + "RealtimeSocket", + "Received server member join frame for ${serverMemberJoinFrame.user} in ${serverMemberJoinFrame.id}." + ) + + val member = fetchMember(serverMemberJoinFrame.id, serverMemberJoinFrame.user) + + RevoltAPI.members.setMember(serverMemberJoinFrame.id, member) + } + + "ServerMemberLeave" -> { + val serverMemberLeaveFrame = + RevoltJson.decodeFromString(ServerMemberLeaveFrame.serializer(), rawFrame) + Log.d( + "RealtimeSocket", + "Received server member leave frame for ${serverMemberLeaveFrame.user} in ${serverMemberLeaveFrame.id}." + ) + + RevoltAPI.members.removeMember( + serverMemberLeaveFrame.id, + serverMemberLeaveFrame.user + ) + } + "Authenticated" -> { /* no-op */ } 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 ef7740af..0938a454 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 @@ -4,7 +4,6 @@ import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltError import chat.revolt.api.RevoltHttp 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 @@ -54,8 +53,8 @@ suspend fun fetchMembers( } membersResponse.members.forEach { member -> - if (!Members.hasMember(serverId, member.id.user)) { - Members.addMember(serverId, member) + if (!RevoltAPI.members.hasMember(serverId, member.id.user)) { + RevoltAPI.members.setMember(serverId, member) } } @@ -81,8 +80,8 @@ suspend fun fetchMember(serverId: String, userId: String, pure: Boolean = false) val member = RevoltJson.decodeFromString(Member.serializer(), response.bodyAsText()) if (!pure) { - if (!Members.hasMember(serverId, member.id.user)) { - Members.addMember(serverId, member) + if (!RevoltAPI.members.hasMember(serverId, member.id.user)) { + RevoltAPI.members.setMember(serverId, member) } } diff --git a/app/src/main/java/chat/revolt/api/schemas/Channel.kt b/app/src/main/java/chat/revolt/api/schemas/Channel.kt index 4408c3e2..556f4597 100644 --- a/app/src/main/java/chat/revolt/api/schemas/Channel.kt +++ b/app/src/main/java/chat/revolt/api/schemas/Channel.kt @@ -1,9 +1,13 @@ package chat.revolt.api.schemas -import kotlinx.serialization.* -import kotlinx.serialization.descriptors.* -import kotlinx.serialization.encoding.* -import kotlinx.serialization.json.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder @Serializable data class MessagesInChannel( @@ -29,7 +33,17 @@ data class Member( val avatar: AutumnResource? = null, val roles: List? = null, val nickname: String? = null -) +) { + fun mergeWithPartial(other: Member): Member { + return Member( + id = other.id, + joinedAt = other.joinedAt ?: joinedAt, + avatar = other.avatar ?: avatar, + roles = other.roles ?: roles, + nickname = other.nickname ?: nickname + ) + } +} @Serializable data class Channel( diff --git a/app/src/main/java/chat/revolt/api/schemas/Server.kt b/app/src/main/java/chat/revolt/api/schemas/Server.kt index 1207b522..9b7606cc 100644 --- a/app/src/main/java/chat/revolt/api/schemas/Server.kt +++ b/app/src/main/java/chat/revolt/api/schemas/Server.kt @@ -20,7 +20,26 @@ data class Server( val flags: Long? = null, val analytics: Boolean? = null, val discoverable: Boolean? = null, -) +) { + fun mergeWithPartial(other: Server): Server { + return Server( + id = other.id ?: id, + owner = other.owner ?: owner, + name = other.name ?: name, + description = other.description ?: description, + channels = other.channels ?: channels, + categories = other.categories ?: categories, + systemMessages = other.systemMessages ?: systemMessages, + roles = other.roles ?: roles, + defaultPermissions = other.defaultPermissions ?: defaultPermissions, + icon = other.icon ?: icon, + banner = other.banner ?: banner, + flags = other.flags ?: flags, + analytics = other.analytics ?: analytics, + discoverable = other.discoverable ?: discoverable, + ) + } +} @Serializable data class Category( 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 2b85795e..292f2392 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,7 +11,6 @@ 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 @@ -129,8 +128,8 @@ class ChannelScreenViewModel : ViewModel() { } it.members?.forEach { member -> - if (!Members.hasMember(member.id.server, member.id.user)) { - Members.addMember(member.id.server, member) + if (!RevoltAPI.members.hasMember(member.id.server, member.id.user)) { + RevoltAPI.members.setMember(member.id.server, member) } } } diff --git a/app/src/main/java/chat/revolt/sheets/UserContextSheet.kt b/app/src/main/java/chat/revolt/sheets/UserContextSheet.kt index f25a785b..3996ee7f 100644 --- a/app/src/main/java/chat/revolt/sheets/UserContextSheet.kt +++ b/app/src/main/java/chat/revolt/sheets/UserContextSheet.kt @@ -26,7 +26,6 @@ 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.WebCompat import chat.revolt.api.internals.solidColor import chat.revolt.api.routes.user.fetchUserProfile @@ -44,7 +43,7 @@ fun UserContextSheet( ) { val user = RevoltAPI.userCache[userId] - val member = serverId?.let { Members.getMember(it, userId) } + val member = serverId?.let { RevoltAPI.members.getMember(it, userId) } val server = RevoltAPI.serverCache[serverId]