mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
+23
-27
@@ -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<WebSocketMessage>(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<WebSocketMessage>(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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,8 +94,9 @@ fun PublicChatScreen() {
|
||||
}
|
||||
|
||||
WebSocketManager.connect()
|
||||
WebSocketManager.setGlobalMessageHandler { msg ->
|
||||
WebSocketManager.addGlobalMessageHandler { msg ->
|
||||
scope.launch {
|
||||
if (msg.type == "newMessage") {
|
||||
try {
|
||||
messages += Json.decodeFromJsonElement<Message>(msg.data!!)
|
||||
delay(500)
|
||||
@@ -100,6 +106,7 @@ fun PublicChatScreen() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delay(500)
|
||||
scrollState.animateScrollTo(scrollState.maxValue)
|
||||
@@ -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<SendMessageResponse>(
|
||||
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("")
|
||||
}
|
||||
}
|
||||
) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user