diff --git a/app/src/main/java/ru/fromchat/api/WebSocketAPI.kt b/app/src/main/java/ru/fromchat/api/WebSocketManager.kt similarity index 77% rename from app/src/main/java/ru/fromchat/api/WebSocketAPI.kt rename to app/src/main/java/ru/fromchat/api/WebSocketManager.kt index efe3ae4..da62b33 100644 --- a/app/src/main/java/ru/fromchat/api/WebSocketAPI.kt +++ b/app/src/main/java/ru/fromchat/api/WebSocketManager.kt @@ -13,7 +13,6 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.cancel -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -22,6 +21,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import kotlinx.serialization.json.Json import ru.fromchat.API_HOST +import kotlin.coroutines.suspendCoroutine object WebSocketManager { // Config @@ -32,10 +32,14 @@ object WebSocketManager { val messages = _messages.asSharedFlow() @Volatile - private var globalHandler: ((WebSocketMessage) -> Unit)? = null + private var globalHandlers = mutableListOf<((WebSocketMessage) -> Unit)>() - fun setGlobalMessageHandler(handler: ((WebSocketMessage) -> Unit)?) { - globalHandler = handler + fun addGlobalMessageHandler(handler: ((WebSocketMessage) -> Unit)) { + globalHandlers += handler + } + + fun removeGlobalMessageHandler(handler: ((WebSocketMessage) -> Unit)) { + globalHandlers -= handler } // State @@ -69,7 +73,7 @@ object WebSocketManager { Log.d("WebSocketManager", "Received payload: $text") try { val msg = json.decodeFromString(text) - globalHandler?.invoke(msg) + globalHandlers.forEach { it(msg) } _messages.emit(msg) } catch (e: Throwable) { Log.w("WebSocketManager", "Received malformed payload:", e) @@ -89,38 +93,30 @@ object WebSocketManager { } suspend fun send(message: WebSocketMessage) { - val payload = json.encodeToString(WebSocketMessage.serializer(), message) - ApiClient.http.webSocket( - method = HttpMethod.Get, - host = API_HOST, - request = { - url { - protocol = URLProtocol.WSS - host = "fromchat.ru" - encodedPath = "/api/chat/ws" - } - } - ) { - send(Frame.Text(payload)) - } + session?.send(Frame.Text(json.encodeToString(message))) } @OptIn(DelicateCoroutinesApi::class) suspend fun request(message: WebSocketMessage, timeoutMs: Long = 10_000): WebSocketMessage? { - val ch = Channel(capacity = 1) - val handler: (WebSocketMessage) -> Unit = { - ch.trySend(it) - } - setGlobalMessageHandler(handler) + Log.d("WebSocketManager", "WebSocket request: $message") + var handler: ((WebSocketMessage) -> Unit)? = null return try { send(message) - withTimeout(timeoutMs) { ch.receive() } + withTimeout(timeoutMs) { + suspendCoroutine { continuation -> + handler = { + continuation.resumeWith(Result.success(it)) + removeGlobalMessageHandler(handler!!) + } + addGlobalMessageHandler(handler) + } + } } catch (_: TimeoutCancellationException) { + Log.w("WebSocketManager", "Request timed out") null } finally { - setGlobalMessageHandler(null) - ch.close() + handler?.let { removeGlobalMessageHandler(it) } } } diff --git a/app/src/main/java/ru/fromchat/ui/PublicChatScreen.kt b/app/src/main/java/ru/fromchat/ui/PublicChatScreen.kt index 799133c..3c14de8 100644 --- a/app/src/main/java/ru/fromchat/ui/PublicChatScreen.kt +++ b/app/src/main/java/ru/fromchat/ui/PublicChatScreen.kt @@ -61,11 +61,16 @@ import kotlinx.datetime.format import kotlinx.datetime.toLocalDateTime import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.encodeToJsonElement import ru.fromchat.DATETIME_FORMAT import ru.fromchat.R import ru.fromchat.api.ApiClient import ru.fromchat.api.Message +import ru.fromchat.api.SendMessageRequest +import ru.fromchat.api.SendMessageResponse +import ru.fromchat.api.WebSocketCredentials import ru.fromchat.api.WebSocketManager +import ru.fromchat.api.WebSocketMessage import ru.fromchat.api.apiRequest import ru.fromchat.utils.exclude import kotlin.time.ExperimentalTime @@ -89,14 +94,16 @@ fun PublicChatScreen() { } WebSocketManager.connect() - WebSocketManager.setGlobalMessageHandler { msg -> + WebSocketManager.addGlobalMessageHandler { msg -> scope.launch { - try { - messages += Json.decodeFromJsonElement(msg.data!!) - delay(500) - scrollState.animateScrollTo(scrollState.maxValue) - } catch (e: Exception) { - Log.e("PublicChatScreen", "Failed to process incoming message:", e) + if (msg.type == "newMessage") { + try { + messages += Json.decodeFromJsonElement(msg.data!!) + delay(500) + scrollState.animateScrollTo(scrollState.maxValue) + } catch (e: Exception) { + Log.e("PublicChatScreen", "Failed to process incoming message:", e) + } } } } @@ -132,6 +139,7 @@ fun PublicChatScreen() { bottomBar = { Row( Modifier + .clip(RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)) .background(MaterialTheme.colorScheme.surfaceContainer) .fillMaxWidth() .windowInsetsPadding(WindowInsets.safeDrawing.exclude(WindowInsetsSides.Top)) @@ -162,17 +170,30 @@ fun PublicChatScreen() { IconButton( onClick = { scope.launch { - val response = apiRequest { - ApiClient.send(message.text.toString()) - } - - message.setTextAndPlaceCursorAtEnd("") - - if (response.isSuccess) { - messages += response.getOrThrow().message + runCatching { + Json.decodeFromJsonElement( + WebSocketManager.request( + WebSocketMessage( + type = "sendMessage", + credentials = WebSocketCredentials( + scheme = "Bearer", + credentials = ApiClient.token!! + ), + data = Json.encodeToJsonElement( + SendMessageRequest( + content = "${message.text}" + ) + ) + ) + )!!.data!! + ) + }.getOrNull()?.let { + messages += it.message delay(500) scrollState.animateScrollTo(scrollState.maxValue) } + + message.setTextAndPlaceCursorAtEnd("") } } ) { diff --git a/app/src/main/java/ru/fromchat/ui/main/MainScreen.kt b/app/src/main/java/ru/fromchat/ui/main/MainScreen.kt index 67cbfe2..e80b1e8 100644 --- a/app/src/main/java/ru/fromchat/ui/main/MainScreen.kt +++ b/app/src/main/java/ru/fromchat/ui/main/MainScreen.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.material.icons.Icons @@ -52,7 +53,8 @@ fun MainScreen() { ) } }, - contentWindowInsets = WindowInsets.safeDrawing.exclude(WindowInsetsSides.Top) + contentWindowInsets = WindowInsets.safeDrawing.exclude(WindowInsetsSides.Top), + modifier = Modifier.imePadding() ) { innerPadding -> Column( Modifier