Rewrite using ChatPanel

Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
This commit is contained in:
2026-02-03 17:35:56 +03:00
Unverified
parent e92578c364
commit ed92ab807d
7 changed files with 265 additions and 293 deletions
@@ -6,12 +6,26 @@ 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.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.BugReport import androidx.compose.material.icons.filled.BugReport
import androidx.compose.runtime.* import androidx.compose.material3.Button
import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pr0gramm3r101.utils.settings.settings import com.pr0gramm3r101.utils.settings.settings
@@ -42,7 +56,7 @@ actual fun DebugApiScreen() {
TopAppBar( TopAppBar(
navigationIcon = { navigationIcon = {
IconButton(onClick = { navController.navigateUp() }) { IconButton(onClick = { navController.navigateUp() }) {
Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = null) Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
} }
}, },
title = { title = {
@@ -155,7 +169,11 @@ actual fun DebugApiScreen() {
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Divider(modifier = Modifier.padding(vertical = 8.dp)) HorizontalDivider(
modifier = Modifier.padding(vertical = 8.dp),
thickness = DividerDefaults.Thickness,
color = DividerDefaults.color
)
Text( Text(
text = "DM Send Test", text = "DM Send Test",
@@ -210,7 +228,11 @@ actual fun DebugApiScreen() {
Text(text = "Send DM") Text(text = "Send DM")
} }
Divider(modifier = Modifier.padding(vertical = 8.dp)) HorizontalDivider(
modifier = Modifier.padding(vertical = 8.dp),
thickness = DividerDefaults.Thickness,
color = DividerDefaults.color
)
Text( Text(
text = "DM Decryption Test", text = "DM Decryption Test",
@@ -188,8 +188,7 @@ fun App(scrollToMessageId: Int? = null, startAtPublicChat: Boolean = false) {
composable("dm/{otherUserId}") { backStackEntry -> composable("dm/{otherUserId}") { backStackEntry ->
val otherUserId = backStackEntry.arguments?.getString("otherUserId")?.toIntOrNull() ?: 0 val otherUserId = backStackEntry.arguments?.getString("otherUserId")?.toIntOrNull() ?: 0
DmScreen( DmScreen(
otherUserId = otherUserId, otherUserId = otherUserId
onBack = { navController.navigateUp() }
) )
} }
@@ -23,7 +23,14 @@ data class ChatPanelState(
val isLoading: Boolean = false, val isLoading: Boolean = false,
val hasMoreMessages: Boolean = false, val hasMoreMessages: Boolean = false,
val isLoadingMore: Boolean = false, val isLoadingMore: Boolean = false,
val typingUsers: List<TypingUser> = emptyList() val typingUsers: List<TypingUser> = emptyList(),
val titleAvatar: AvatarInfo? = null
)
@Serializable
data class AvatarInfo(
val displayName: String,
val profilePictureUrl: String? = null
) )
/** /**
@@ -308,5 +315,8 @@ abstract class ChatPanel(
// Abstract UI control methods // Abstract UI control methods
abstract fun showCallButton(): Boolean abstract fun showCallButton(): Boolean
abstract fun getTypingHandler(): TypingHandler abstract fun getTypingHandler(): TypingHandler
open val showUsernamesInMessages: Boolean
get() = true
} }
@@ -8,6 +8,7 @@ import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement 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.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -15,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@@ -67,6 +69,7 @@ import ru.fromchat.api.WebSocketUpdatesData
import ru.fromchat.back import ru.fromchat.back
import ru.fromchat.core.Logger import ru.fromchat.core.Logger
import ru.fromchat.ui.LocalNavController import ru.fromchat.ui.LocalNavController
import ru.fromchat.ui.chat.Avatar
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
@Composable @Composable
@@ -162,7 +165,9 @@ fun ChatScreen(
data = update.data data = update.data
) )
when (update.type) { when (update.type) {
"newMessage", "messageEdited", "messageDeleted", "typing", "stopTyping", "statusUpdate", "suspended", "account_deleted" -> { "newMessage", "messageEdited", "messageDeleted",
"dmNew", "dmEdited", "dmDeleted",
"typing", "stopTyping", "statusUpdate", "suspended", "account_deleted" -> {
Logger.d("ChatScreen", "Launching handleWebSocketMessage for ${update.type}") Logger.d("ChatScreen", "Launching handleWebSocketMessage for ${update.type}")
scope.launch { scope.launch {
try { try {
@@ -179,7 +184,7 @@ fun ChatScreen(
e.printStackTrace() e.printStackTrace()
} }
} }
"newMessage", "messageEdited", "messageDeleted" -> { "newMessage", "messageEdited", "messageDeleted", "dmNew", "dmEdited", "dmDeleted" -> {
scope.launch { scope.launch {
panel.handleWebSocketMessage(message) panel.handleWebSocketMessage(message)
} }
@@ -205,6 +210,18 @@ fun ChatScreen(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { title = {
Row(
verticalAlignment = Alignment.CenterVertically
) {
panelState.titleAvatar?.let { avatar ->
Avatar(
profilePictureUrl = avatar.profilePictureUrl,
displayName = avatar.displayName,
size = 36.dp
)
Spacer(modifier = Modifier.width(8.dp))
}
Column(Modifier.fillMaxWidth()) { Column(Modifier.fillMaxWidth()) {
Text( Text(
text = panelState.title, text = panelState.title,
@@ -226,6 +243,7 @@ fun ChatScreen(
} }
} }
} }
}
}, },
navigationIcon = { navigationIcon = {
IconButton(onClick = { navController.navigateUp() }) { IconButton(onClick = { navController.navigateUp() }) {
@@ -360,7 +378,8 @@ fun ChatScreen(
}, },
onTapPosition = { offset -> onTapPosition = { offset ->
tapOffset = offset tapOffset = offset
} },
showUsername = panel.showUsernamesInMessages
) )
} }
} }
@@ -49,7 +49,8 @@ fun MessageItem(
isAuthor: Boolean, isAuthor: Boolean,
onLongPress: () -> Unit, onLongPress: () -> Unit,
onTapPosition: (Offset) -> Unit = {}, onTapPosition: (Offset) -> Unit = {},
modifier: Modifier = Modifier modifier: Modifier = Modifier,
showUsername: Boolean = true
) { ) {
AnimatedVisibility( AnimatedVisibility(
visible = true, visible = true,
@@ -78,8 +79,7 @@ fun MessageItem(
horizontalArrangement = if (isAuthor) Arrangement.End else Arrangement.Start, horizontalArrangement = if (isAuthor) Arrangement.End else Arrangement.Start,
verticalAlignment = Alignment.Bottom verticalAlignment = Alignment.Bottom
) { ) {
if (!isAuthor) { if (!isAuthor && showUsername) {
// Avatar at bottom
Avatar( Avatar(
profilePictureUrl = message.profile_picture, profilePictureUrl = message.profile_picture,
displayName = message.username, displayName = message.username,
@@ -132,7 +132,7 @@ fun MessageItem(
) { ) {
Column { Column {
// Username inside bubble (for received messages) // Username inside bubble (for received messages)
if (!isAuthor) { if (showUsername && !isAuthor) {
Text( Text(
text = message.username, text = message.username,
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
@@ -172,6 +172,7 @@ fun MessageItem(
Column( Column(
Modifier.padding(horizontal = 8.dp, vertical = 6.dp) Modifier.padding(horizontal = 8.dp, vertical = 6.dp)
) { ) {
if (showUsername) {
Text( Text(
text = replyTo.username, text = replyTo.username,
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
@@ -179,6 +180,7 @@ fun MessageItem(
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
fontSize = 11.sp fontSize = 11.sp
) )
}
Text( Text(
text = replyTo.content.take(50) + if (replyTo.content.length > 50) "..." else "", text = replyTo.content.take(50) + if (replyTo.content.length > 50) "..." else "",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
@@ -0,0 +1,157 @@
package ru.fromchat.ui.dm
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.decodeFromJsonElement
import ru.fromchat.api.ApiClient
import ru.fromchat.api.DmEnvelope
import ru.fromchat.api.Message
import ru.fromchat.api.WebSocketMessage
import ru.fromchat.core.Logger
import ru.fromchat.crypto.decryptEnvelope
import ru.fromchat.ui.chat.ChatPanel
import ru.fromchat.ui.chat.TypingHandler
import ru.fromchat.ui.chat.TypingUser
import ru.fromchat.ui.chat.AvatarInfo
class DmPanel(
private val otherUserId: Int,
coroutineScope: CoroutineScope,
currentUserId: Int?
) : ChatPanel(
id = "dm-$otherUserId",
currentUserId = currentUserId,
scope = coroutineScope
) {
private val typingHandler = NoopTypingHandler()
private val json = Json { ignoreUnknownKeys = true }
private var otherDisplayName: String = "User $otherUserId"
private var otherProfilePicture: String? = null
init {
updateState { it.copy(title = "Direct message") }
coroutineScope.launch(Dispatchers.IO) {
runCatching {
ApiClient.getProfileById(otherUserId)
}.onSuccess { profile ->
val displayName = profile.displayName?.takeIf { it.isNotBlank() } ?: profile.username
otherDisplayName = displayName
otherProfilePicture = profile.profilePicture
updateState {
it.copy(
title = displayName,
titleAvatar = AvatarInfo(
displayName = displayName,
profilePictureUrl = otherProfilePicture
)
)
}
}
}
}
override suspend fun sendMessage(content: String, replyToId: Int?, clientMessageId: String?) {
ApiClient.sendDm(recipientId = otherUserId, plaintext = content)
}
override suspend fun loadMessages() {
setLoading(true)
runCatching {
ApiClient.getDmHistory(otherUserId)
}.onSuccess { response ->
clearMessages()
response.messages.mapNotNull { envelope ->
decryptEnvelope(envelope, currentUserId)?.let { plaintext ->
createMessage(envelope, plaintext)
}
}.forEach { addMessage(it) }
setHasMoreMessages(false)
}.onFailure { error ->
Logger.e("DmPanel", "Failed to load DM history: ${error.message}", error)
}
setLoading(false)
}
override suspend fun loadMoreMessages() {
// Not implemented yet
}
override suspend fun handleWebSocketMessage(message: WebSocketMessage) {
when (message.type) {
"dmNew" -> message.data?.let { processEnvelope(it) }
"updates" -> {
val updates = message.data?.jsonObject?.get("updates")?.jsonArray ?: return
for (update in updates) {
val obj = update.jsonObject
if (obj["type"]?.jsonPrimitive?.content == "dmNew") {
obj["data"]?.let { processEnvelope(it) }
}
}
}
}
}
private fun processEnvelope(element: JsonElement) {
val envelope = runCatching {
json.decodeFromJsonElement(DmEnvelope.serializer(), element)
}.getOrNull() ?: return
if (envelope.senderId != otherUserId && envelope.recipientId != otherUserId) return
scope.launch(Dispatchers.IO) {
val plaintext = runCatching { decryptEnvelope(envelope, currentUserId) }.getOrNull()
if (plaintext != null) {
addMessage(createMessage(envelope, plaintext))
}
}
}
private fun createMessage(envelope: DmEnvelope, plaintext: String): Message {
val username = if (envelope.senderId == currentUserId) {
"You"
} else {
otherDisplayName
}
return Message(
id = envelope.id,
user_id = envelope.senderId,
content = plaintext,
timestamp = envelope.timestamp,
is_read = envelope.recipientId == currentUserId,
is_edited = false,
username = username,
profile_picture = null,
verified = null,
reply_to = null,
client_message_id = null,
reactions = null
)
}
override suspend fun handleEditMessage(messageId: Int, content: String) {}
override suspend fun handleDeleteMessage(messageId: Int) {}
override fun showCallButton(): Boolean = false
override fun getTypingHandler(): TypingHandler = typingHandler
override val showUsernamesInMessages: Boolean
get() = false
}
private class NoopTypingHandler : TypingHandler {
private val _typingUsers = MutableStateFlow<List<TypingUser>>(emptyList())
override val typingUsers: StateFlow<List<TypingUser>> = _typingUsers.asStateFlow()
override fun sendTyping() {}
override fun stopTyping() {}
override fun handleTypingEvent(userId: Int, username: String) {}
override fun handleStopTypingEvent(userId: Int) {}
}
@@ -1,276 +1,39 @@
package ru.fromchat.ui.dm package ru.fromchat.ui.dm
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.serializer
import ru.fromchat.api.ApiClient import ru.fromchat.api.ApiClient
import ru.fromchat.api.DmEnvelope import ru.fromchat.ui.chat.ChatScreen
import ru.fromchat.api.WebSocketManager
import ru.fromchat.api.WebSocketMessage
import ru.fromchat.crypto.decryptEnvelope
private data class DecryptedDmMessage(
val id: Int,
val text: String,
val timestamp: String,
val isOutgoing: Boolean
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun DmScreen( fun DmScreen(
otherUserId: Int, otherUserId: Int,
onBack: () -> Unit modifier: Modifier = Modifier
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val messages = remember { mutableStateListOf<DecryptedDmMessage>() }
var isLoading by remember { mutableStateOf(false) }
var status by remember { mutableStateOf<String?>(null) }
var input by remember { mutableStateOf("") }
val currentUserId = ApiClient.user?.id val currentUserId = ApiClient.user?.id
val json = Json { ignoreUnknownKeys = true } val panel = remember(otherUserId) {
DmPanel(
suspend fun loadHistory() { otherUserId = otherUserId,
isLoading = true coroutineScope = coroutineScope,
runCatching { currentUserId = currentUserId
ApiClient.getDmHistory(otherUserId)
}.onSuccess { response ->
val decrypted = response.messages.mapNotNull { envelope ->
val plaintext = runCatching { decryptEnvelope(envelope, currentUserId) }.getOrNull()
plaintext?.let {
DecryptedDmMessage(
id = envelope.id,
text = it,
timestamp = envelope.timestamp,
isOutgoing = currentUserId != null && envelope.senderId == currentUserId
) )
} }
}
messages.clear()
messages.addAll(decrypted)
status = null
}.onFailure {
status = it.message ?: "Failed to load DMs"
}
isLoading = false
}
LaunchedEffect(otherUserId) { LaunchedEffect(otherUserId) {
loadHistory()
}
DisposableEffect(otherUserId) {
val handler: (WebSocketMessage) -> Unit = handler@ { msg ->
val payloads = extractDmPayloads(msg)
for (payload in payloads) {
val envelope = runCatching {
json.decodeFromJsonElement(DmEnvelope.serializer(), payload)
}.getOrNull() ?: continue
if (envelope.senderId != otherUserId && envelope.recipientId != otherUserId) continue
coroutineScope.launch { coroutineScope.launch {
val plaintext = runCatching { decryptEnvelope(envelope, currentUserId) }.getOrNull() panel.loadMessages()
if (plaintext == null) return@launch
val newMessage = DecryptedDmMessage(
id = envelope.id,
text = plaintext,
timestamp = envelope.timestamp,
isOutgoing = currentUserId != null && envelope.senderId == currentUserId
)
if (messages.none { it.id == newMessage.id }) {
messages.add(newMessage)
}
}
}
}
WebSocketManager.addGlobalMessageHandler(handler)
onDispose {
WebSocketManager.removeGlobalMessageHandler(handler)
} }
} }
Scaffold( ChatScreen(
topBar = { panel = panel,
TopAppBar( currentUserId = currentUserId,
title = { Text(text = "DM with user $otherUserId") }, modifier = modifier.fillMaxSize()
navigationIcon = {
IconButton(onClick = onBack) {
Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = "Back")
}
}
) )
}
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
if (isLoading) {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else {
LazyColumn(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
if (messages.isEmpty()) {
item {
Text(
text = "No messages yet.",
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
} else {
items(messages, key = { it.id }) { message ->
MessageBubble(message = message)
}
}
}
}
status?.let {
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error
)
}
Divider()
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.Bottom
) {
OutlinedTextField(
value = input,
onValueChange = { input = it },
modifier = Modifier.weight(1f),
label = { Text("Message") },
singleLine = false
)
Button(
onClick = {
if (input.isBlank()) return@Button
coroutineScope.launch {
runCatching {
ApiClient.sendDm(recipientId = otherUserId, plaintext = input.trim())
}.onSuccess {
input = ""
loadHistory()
}.onFailure {
status = it.message ?: "Failed to send DM"
}
}
}
) {
Text(text = "Send")
}
}
}
}
}
@Composable
private fun MessageBubble(message: DecryptedDmMessage) {
val alignment = if (message.isOutgoing) Alignment.End else Alignment.Start
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = alignment
) {
Card(
modifier = Modifier
.fillMaxWidth(0.85f),
shape = MaterialTheme.shapes.medium
) {
Column(modifier = Modifier.padding(12.dp)) {
Text(text = message.text, style = MaterialTheme.typography.bodyMedium)
Spacer(modifier = Modifier.size(4.dp))
Text(
text = message.timestamp,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
private fun extractDmPayloads(msg: WebSocketMessage): List<JsonElement> {
val results = mutableListOf<JsonElement>()
when (msg.type) {
"dmNew" -> msg.data?.let { results.add(it) }
"updates" -> {
val updatesArray = msg.data
?.jsonObject
?.get("updates")
?.jsonArray
?: JsonArray(emptyList())
for (item in updatesArray) {
val obj = item.jsonObject
if (obj["type"]?.jsonPrimitive?.content == "dmNew") {
obj["data"]?.let { results.add(it) }
}
}
}
}
return results
} }