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.Dispatchers
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
@@ -22,6 +21,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import ru.fromchat.API_HOST
|
import ru.fromchat.API_HOST
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
object WebSocketManager {
|
object WebSocketManager {
|
||||||
// Config
|
// Config
|
||||||
@@ -32,10 +32,14 @@ object WebSocketManager {
|
|||||||
val messages = _messages.asSharedFlow()
|
val messages = _messages.asSharedFlow()
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var globalHandler: ((WebSocketMessage) -> Unit)? = null
|
private var globalHandlers = mutableListOf<((WebSocketMessage) -> Unit)>()
|
||||||
|
|
||||||
fun setGlobalMessageHandler(handler: ((WebSocketMessage) -> Unit)?) {
|
fun addGlobalMessageHandler(handler: ((WebSocketMessage) -> Unit)) {
|
||||||
globalHandler = handler
|
globalHandlers += handler
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeGlobalMessageHandler(handler: ((WebSocketMessage) -> Unit)) {
|
||||||
|
globalHandlers -= handler
|
||||||
}
|
}
|
||||||
|
|
||||||
// State
|
// State
|
||||||
@@ -69,7 +73,7 @@ object WebSocketManager {
|
|||||||
Log.d("WebSocketManager", "Received payload: $text")
|
Log.d("WebSocketManager", "Received payload: $text")
|
||||||
try {
|
try {
|
||||||
val msg = json.decodeFromString<WebSocketMessage>(text)
|
val msg = json.decodeFromString<WebSocketMessage>(text)
|
||||||
globalHandler?.invoke(msg)
|
globalHandlers.forEach { it(msg) }
|
||||||
_messages.emit(msg)
|
_messages.emit(msg)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.w("WebSocketManager", "Received malformed payload:", e)
|
Log.w("WebSocketManager", "Received malformed payload:", e)
|
||||||
@@ -89,38 +93,30 @@ object WebSocketManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun send(message: WebSocketMessage) {
|
suspend fun send(message: WebSocketMessage) {
|
||||||
val payload = json.encodeToString(WebSocketMessage.serializer(), message)
|
session?.send(Frame.Text(json.encodeToString(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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
suspend fun request(message: WebSocketMessage, timeoutMs: Long = 10_000): WebSocketMessage? {
|
suspend fun request(message: WebSocketMessage, timeoutMs: Long = 10_000): WebSocketMessage? {
|
||||||
val ch = Channel<WebSocketMessage>(capacity = 1)
|
Log.d("WebSocketManager", "WebSocket request: $message")
|
||||||
val handler: (WebSocketMessage) -> Unit = {
|
var handler: ((WebSocketMessage) -> Unit)? = null
|
||||||
ch.trySend(it)
|
|
||||||
}
|
|
||||||
setGlobalMessageHandler(handler)
|
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
send(message)
|
send(message)
|
||||||
withTimeout(timeoutMs) { ch.receive() }
|
withTimeout(timeoutMs) {
|
||||||
|
suspendCoroutine { continuation ->
|
||||||
|
handler = {
|
||||||
|
continuation.resumeWith(Result.success(it))
|
||||||
|
removeGlobalMessageHandler(handler!!)
|
||||||
|
}
|
||||||
|
addGlobalMessageHandler(handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (_: TimeoutCancellationException) {
|
} catch (_: TimeoutCancellationException) {
|
||||||
|
Log.w("WebSocketManager", "Request timed out")
|
||||||
null
|
null
|
||||||
} finally {
|
} finally {
|
||||||
setGlobalMessageHandler(null)
|
handler?.let { removeGlobalMessageHandler(it) }
|
||||||
ch.close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,11 +61,16 @@ import kotlinx.datetime.format
|
|||||||
import kotlinx.datetime.toLocalDateTime
|
import kotlinx.datetime.toLocalDateTime
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.decodeFromJsonElement
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
import kotlinx.serialization.json.encodeToJsonElement
|
||||||
import ru.fromchat.DATETIME_FORMAT
|
import ru.fromchat.DATETIME_FORMAT
|
||||||
import ru.fromchat.R
|
import ru.fromchat.R
|
||||||
import ru.fromchat.api.ApiClient
|
import ru.fromchat.api.ApiClient
|
||||||
import ru.fromchat.api.Message
|
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.WebSocketManager
|
||||||
|
import ru.fromchat.api.WebSocketMessage
|
||||||
import ru.fromchat.api.apiRequest
|
import ru.fromchat.api.apiRequest
|
||||||
import ru.fromchat.utils.exclude
|
import ru.fromchat.utils.exclude
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
@@ -89,8 +94,9 @@ fun PublicChatScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WebSocketManager.connect()
|
WebSocketManager.connect()
|
||||||
WebSocketManager.setGlobalMessageHandler { msg ->
|
WebSocketManager.addGlobalMessageHandler { msg ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
if (msg.type == "newMessage") {
|
||||||
try {
|
try {
|
||||||
messages += Json.decodeFromJsonElement<Message>(msg.data!!)
|
messages += Json.decodeFromJsonElement<Message>(msg.data!!)
|
||||||
delay(500)
|
delay(500)
|
||||||
@@ -100,6 +106,7 @@ fun PublicChatScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delay(500)
|
delay(500)
|
||||||
scrollState.animateScrollTo(scrollState.maxValue)
|
scrollState.animateScrollTo(scrollState.maxValue)
|
||||||
@@ -132,6 +139,7 @@ fun PublicChatScreen() {
|
|||||||
bottomBar = {
|
bottomBar = {
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
|
.clip(RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp))
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.windowInsetsPadding(WindowInsets.safeDrawing.exclude(WindowInsetsSides.Top))
|
.windowInsetsPadding(WindowInsets.safeDrawing.exclude(WindowInsetsSides.Top))
|
||||||
@@ -162,17 +170,30 @@ fun PublicChatScreen() {
|
|||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val response = apiRequest {
|
runCatching {
|
||||||
ApiClient.send(message.text.toString())
|
Json.decodeFromJsonElement<SendMessageResponse>(
|
||||||
}
|
WebSocketManager.request(
|
||||||
|
WebSocketMessage(
|
||||||
message.setTextAndPlaceCursorAtEnd("")
|
type = "sendMessage",
|
||||||
|
credentials = WebSocketCredentials(
|
||||||
if (response.isSuccess) {
|
scheme = "Bearer",
|
||||||
messages += response.getOrThrow().message
|
credentials = ApiClient.token!!
|
||||||
|
),
|
||||||
|
data = Json.encodeToJsonElement(
|
||||||
|
SendMessageRequest(
|
||||||
|
content = "${message.text}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)!!.data!!
|
||||||
|
)
|
||||||
|
}.getOrNull()?.let {
|
||||||
|
messages += it.message
|
||||||
delay(500)
|
delay(500)
|
||||||
scrollState.animateScrollTo(scrollState.maxValue)
|
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.WindowInsets
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
import androidx.compose.material.icons.Icons
|
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 ->
|
) { innerPadding ->
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
|
|||||||
Reference in New Issue
Block a user