diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatScreen.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatScreen.kt index 1aeff25..ac24247 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatScreen.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatScreen.kt @@ -231,13 +231,11 @@ fun ChatScreen( "newMessage", "messageEdited", "messageDeleted", "dmNew", "dmEdited", "dmDeleted", "typing", "stopTyping", "dmTyping", "stopDmTyping", "suspended", "account_deleted" -> { - Logger.d("ChatScreen", "Launching handleWebSocketMessage for ${update.type}") - scope.launch { - try { - panel.handleWebSocketMessage(wsMessage) - } catch (e: Exception) { - Logger.e("ChatScreen", "Error handling WebSocket message: ${e.message}", e) - } + Logger.d("ChatScreen", "handleWebSocketMessage for ${update.type}") + try { + panel.handleWebSocketMessage(wsMessage) + } catch (e: Exception) { + Logger.e("ChatScreen", "Error handling WebSocket message: ${e.message}", e) } } } @@ -259,6 +257,11 @@ fun ChatScreen( panel.handleWebSocketMessage(message) } } + "sendMessage" -> { + scope.launch { + panel.handleWebSocketMessage(message) + } + } else -> { Logger.d("ChatScreen", "Unhandled top-level WebSocket message type: ${message.type}") } diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/PublicChatPanel.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/PublicChatPanel.kt index 631ef1c..c0b8b6e 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/PublicChatPanel.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/PublicChatPanel.kt @@ -8,6 +8,7 @@ import ru.fromchat.api.ApiClient import ru.fromchat.api.Message import ru.fromchat.api.MessageDeletedData import ru.fromchat.api.ReactionUpdateData +import ru.fromchat.api.SendMessageResponse import ru.fromchat.api.TypingUpdateData import ru.fromchat.api.WebSocketMessage 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?) { ApiClient.sendMessage(content, replyToId, clientMessageId) } @@ -185,12 +222,18 @@ class PublicChatPanel( val data = updateMessage.data ?: return val newMsg = json.decodeFromJsonElement(Message.serializer(), data) Logger.d("PublicChatPanel", "New message received: id=${newMsg.id}, content=${newMsg.content.take(50)}") - - if (newMsg.client_message_id != null && newMsg.user_id == currentUserId) { - handleMessageConfirmed(newMsg.client_message_id, newMsg) - } else { - addMessage(newMsg) - } + confirmIncomingOwnMessageOrAdd(newMsg) + } + "sendMessage" -> { + val data = updateMessage.data ?: return + 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" -> { val data = updateMessage.data ?: return diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/SettingsTab.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/SettingsTab.kt index eab9ecf..c5d2bd4 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/SettingsTab.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/SettingsTab.kt @@ -111,6 +111,7 @@ fun SettingsTab( Settings.materialYou && materialYouAvailable ) } + var themeChipIndex by remember { mutableIntStateOf(Settings.theme.ordinal) } Category(Modifier.padding(top = 16.dp)) { // Material You @@ -146,7 +147,6 @@ fun SettingsTab( }, bottomContent = { FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - var selectedIndex by remember { mutableIntStateOf(Settings.theme.ordinal) } val options = listOf( stringResource(Res.string.as_system), stringResource(Res.string.light), @@ -155,11 +155,11 @@ fun SettingsTab( options.forEachIndexed { index, label -> FilterChip( onClick = { - selectedIndex = index + themeChipIndex = index Settings.theme = Theme.entries[index] theme = Theme.entries[index] }, - selected = index == selectedIndex, + selected = index == themeChipIndex, leadingIcon = { if (index == 0) { Spacer(Modifier.width(16.dp)) diff --git a/utils/shared/src/commonMain/kotlin/com/pr0gramm3r101/components/Components.kt b/utils/shared/src/commonMain/kotlin/com/pr0gramm3r101/components/Components.kt index 518dadf..381f71e 100644 --- a/utils/shared/src/commonMain/kotlin/com/pr0gramm3r101/components/Components.kt +++ b/utils/shared/src/commonMain/kotlin/com/pr0gramm3r101/components/Components.kt @@ -111,7 +111,7 @@ fun ListItem( onClick: (() -> Unit)? = null, bodyOnClick: (() -> Unit)? = null, leadingAndBodyShared: Boolean = false, - bottomContent: (@Composable ConstraintLayoutScope.() -> Unit)? = null + bottomContent: (@Composable () -> Unit)? = null ) { Column { @Composable @@ -244,7 +244,7 @@ fun ListItem( ) } if (bottomContent != null) { - ConstraintLayout( + Box( modifier = Modifier .constrainAs(btm) { bottom link parent.bottom @@ -252,9 +252,10 @@ fun ListItem( right link parent.right width = Dimension.fillToConstraints } - .padding(start = 16.dp, end = 16.dp, bottom = 12.dp), - content = bottomContent - ) + .padding(start = 16.dp, end = 16.dp, bottom = 12.dp) + ) { + bottomContent() + } } } } @@ -304,11 +305,17 @@ inline fun SwitchListItem( supportingText = supportingText, leadingContent = leadingContent, trailingContent = { + val (sw) = createRefs() Switch( checked = checked, onCheckedChange = onCheckedChange, interactionSource = interactionSource, - enabled = enabled + enabled = enabled, + modifier = Modifier.constrainAs(sw) { + top link parent.top + bottom link parent.bottom + right link parent.right + } ) }, divider = divider,