Fix public chat message sending and Theme switches disappearing

This commit is contained in:
2026-04-03 16:18:04 +03:00
Unverified
parent 58dc1223c2
commit 8cdf5b2167
4 changed files with 75 additions and 22 deletions
@@ -231,13 +231,11 @@ fun ChatScreen(
"newMessage", "messageEdited", "messageDeleted", "newMessage", "messageEdited", "messageDeleted",
"dmNew", "dmEdited", "dmDeleted", "dmNew", "dmEdited", "dmDeleted",
"typing", "stopTyping", "dmTyping", "stopDmTyping", "suspended", "account_deleted" -> { "typing", "stopTyping", "dmTyping", "stopDmTyping", "suspended", "account_deleted" -> {
Logger.d("ChatScreen", "Launching handleWebSocketMessage for ${update.type}") Logger.d("ChatScreen", "handleWebSocketMessage for ${update.type}")
scope.launch { try {
try { panel.handleWebSocketMessage(wsMessage)
panel.handleWebSocketMessage(wsMessage) } catch (e: Exception) {
} catch (e: Exception) { Logger.e("ChatScreen", "Error handling WebSocket message: ${e.message}", e)
Logger.e("ChatScreen", "Error handling WebSocket message: ${e.message}", e)
}
} }
} }
} }
@@ -259,6 +257,11 @@ fun ChatScreen(
panel.handleWebSocketMessage(message) panel.handleWebSocketMessage(message)
} }
} }
"sendMessage" -> {
scope.launch {
panel.handleWebSocketMessage(message)
}
}
else -> { else -> {
Logger.d("ChatScreen", "Unhandled top-level WebSocket message type: ${message.type}") Logger.d("ChatScreen", "Unhandled top-level WebSocket message type: ${message.type}")
} }
@@ -8,6 +8,7 @@ import ru.fromchat.api.ApiClient
import ru.fromchat.api.Message import ru.fromchat.api.Message
import ru.fromchat.api.MessageDeletedData import ru.fromchat.api.MessageDeletedData
import ru.fromchat.api.ReactionUpdateData import ru.fromchat.api.ReactionUpdateData
import ru.fromchat.api.SendMessageResponse
import ru.fromchat.api.TypingUpdateData import ru.fromchat.api.TypingUpdateData
import ru.fromchat.api.WebSocketMessage import ru.fromchat.api.WebSocketMessage
import ru.fromchat.api.WebSocketUpdatesData import ru.fromchat.api.WebSocketUpdatesData
@@ -65,6 +66,42 @@ class PublicChatPanel(
} }
} }
/**
* Server often omits [Message.client_message_id] on broadcast [newMessage] / [SendMessageResponse.message].
* Match the oldest pending optimistic row (same user, text, reply) and replace it; otherwise append.
*/
private suspend fun confirmIncomingOwnMessageOrAdd(newMsg: Message) {
val uid = currentUserId
if (uid == null) {
addMessage(newMsg)
return
}
if (newMsg.user_id != uid) {
addMessage(newMsg)
return
}
if (newMsg.client_message_id != null) {
handleMessageConfirmed(newMsg.client_message_id, newMsg)
return
}
if (newMsg.id <= 0) {
addMessage(newMsg)
return
}
val pending = _state.messages.firstOrNull { msg ->
msg.id < 0 &&
msg.user_id == uid &&
msg.client_message_id != null &&
msg.content == newMsg.content &&
msg.reply_to?.id == newMsg.reply_to?.id
}
if (pending?.client_message_id != null) {
handleMessageConfirmed(pending.client_message_id, newMsg)
} else {
addMessage(newMsg)
}
}
override suspend fun sendMessage(content: String, replyToId: Int?, clientMessageId: String?) { override suspend fun sendMessage(content: String, replyToId: Int?, clientMessageId: String?) {
ApiClient.sendMessage(content, replyToId, clientMessageId) ApiClient.sendMessage(content, replyToId, clientMessageId)
} }
@@ -185,12 +222,18 @@ class PublicChatPanel(
val data = updateMessage.data ?: return val data = updateMessage.data ?: return
val newMsg = json.decodeFromJsonElement(Message.serializer(), data) val newMsg = json.decodeFromJsonElement(Message.serializer(), data)
Logger.d("PublicChatPanel", "New message received: id=${newMsg.id}, content=${newMsg.content.take(50)}") Logger.d("PublicChatPanel", "New message received: id=${newMsg.id}, content=${newMsg.content.take(50)}")
confirmIncomingOwnMessageOrAdd(newMsg)
if (newMsg.client_message_id != null && newMsg.user_id == currentUserId) { }
handleMessageConfirmed(newMsg.client_message_id, newMsg) "sendMessage" -> {
} else { val data = updateMessage.data ?: return
addMessage(newMsg) val resp = json.decodeFromJsonElement(SendMessageResponse.serializer(), data)
} if (!resp.status.equals("success", ignoreCase = true)) return
val confirmed = resp.message
Logger.d(
"PublicChatPanel",
"sendMessage ack: id=${confirmed.id}, clientId=${confirmed.client_message_id}"
)
confirmIncomingOwnMessageOrAdd(confirmed)
} }
"messageEdited" -> { "messageEdited" -> {
val data = updateMessage.data ?: return val data = updateMessage.data ?: return
@@ -111,6 +111,7 @@ fun SettingsTab(
Settings.materialYou && materialYouAvailable Settings.materialYou && materialYouAvailable
) )
} }
var themeChipIndex by remember { mutableIntStateOf(Settings.theme.ordinal) }
Category(Modifier.padding(top = 16.dp)) { Category(Modifier.padding(top = 16.dp)) {
// Material You // Material You
@@ -146,7 +147,6 @@ fun SettingsTab(
}, },
bottomContent = { bottomContent = {
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
var selectedIndex by remember { mutableIntStateOf(Settings.theme.ordinal) }
val options = listOf( val options = listOf(
stringResource(Res.string.as_system), stringResource(Res.string.as_system),
stringResource(Res.string.light), stringResource(Res.string.light),
@@ -155,11 +155,11 @@ fun SettingsTab(
options.forEachIndexed { index, label -> options.forEachIndexed { index, label ->
FilterChip( FilterChip(
onClick = { onClick = {
selectedIndex = index themeChipIndex = index
Settings.theme = Theme.entries[index] Settings.theme = Theme.entries[index]
theme = Theme.entries[index] theme = Theme.entries[index]
}, },
selected = index == selectedIndex, selected = index == themeChipIndex,
leadingIcon = { leadingIcon = {
if (index == 0) { if (index == 0) {
Spacer(Modifier.width(16.dp)) Spacer(Modifier.width(16.dp))
@@ -111,7 +111,7 @@ fun ListItem(
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
bodyOnClick: (() -> Unit)? = null, bodyOnClick: (() -> Unit)? = null,
leadingAndBodyShared: Boolean = false, leadingAndBodyShared: Boolean = false,
bottomContent: (@Composable ConstraintLayoutScope.() -> Unit)? = null bottomContent: (@Composable () -> Unit)? = null
) { ) {
Column { Column {
@Composable @Composable
@@ -244,7 +244,7 @@ fun ListItem(
) )
} }
if (bottomContent != null) { if (bottomContent != null) {
ConstraintLayout( Box(
modifier = Modifier modifier = Modifier
.constrainAs(btm) { .constrainAs(btm) {
bottom link parent.bottom bottom link parent.bottom
@@ -252,9 +252,10 @@ fun ListItem(
right link parent.right right link parent.right
width = Dimension.fillToConstraints width = Dimension.fillToConstraints
} }
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp), .padding(start = 16.dp, end = 16.dp, bottom = 12.dp)
content = bottomContent ) {
) bottomContent()
}
} }
} }
} }
@@ -304,11 +305,17 @@ inline fun SwitchListItem(
supportingText = supportingText, supportingText = supportingText,
leadingContent = leadingContent, leadingContent = leadingContent,
trailingContent = { trailingContent = {
val (sw) = createRefs()
Switch( Switch(
checked = checked, checked = checked,
onCheckedChange = onCheckedChange, onCheckedChange = onCheckedChange,
interactionSource = interactionSource, interactionSource = interactionSource,
enabled = enabled enabled = enabled,
modifier = Modifier.constrainAs(sw) {
top link parent.top
bottom link parent.bottom
right link parent.right
}
) )
}, },
divider = divider, divider = divider,