mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
03e718b743
Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
276 lines
11 KiB
Kotlin
276 lines
11 KiB
Kotlin
package ru.fromchat.ui
|
|
|
|
import android.util.Log
|
|
import androidx.compose.foundation.background
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
import androidx.compose.foundation.layout.Box
|
|
import androidx.compose.foundation.layout.Column
|
|
import androidx.compose.foundation.layout.IntrinsicSize
|
|
import androidx.compose.foundation.layout.Row
|
|
import androidx.compose.foundation.layout.Spacer
|
|
import androidx.compose.foundation.layout.WindowInsets
|
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
|
import androidx.compose.foundation.layout.exclude
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
import androidx.compose.foundation.layout.only
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.foundation.layout.safeDrawing
|
|
import androidx.compose.foundation.layout.size
|
|
import androidx.compose.foundation.layout.width
|
|
import androidx.compose.foundation.layout.widthIn
|
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
|
import androidx.compose.foundation.rememberScrollState
|
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
import androidx.compose.foundation.text.BasicTextField
|
|
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
|
import androidx.compose.foundation.text.input.rememberTextFieldState
|
|
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
|
import androidx.compose.foundation.verticalScroll
|
|
import androidx.compose.material.icons.Icons
|
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
import androidx.compose.material.icons.automirrored.filled.Send
|
|
import androidx.compose.material.icons.filled.DoneAll
|
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
import androidx.compose.material3.Icon
|
|
import androidx.compose.material3.IconButton
|
|
import androidx.compose.material3.LocalTextStyle
|
|
import androidx.compose.material3.MaterialTheme
|
|
import androidx.compose.material3.Scaffold
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.material3.TopAppBar
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.LaunchedEffect
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.mutableStateListOf
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.remember
|
|
import androidx.compose.runtime.rememberCoroutineScope
|
|
import androidx.compose.runtime.setValue
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.draw.clip
|
|
import androidx.compose.ui.graphics.SolidColor
|
|
import androidx.compose.ui.layout.onSizeChanged
|
|
import androidx.compose.ui.platform.LocalDensity
|
|
import androidx.compose.ui.res.stringResource
|
|
import androidx.compose.ui.text.style.TextOverflow
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.compose.ui.unit.sp
|
|
import kotlinx.coroutines.delay
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.datetime.TimeZone
|
|
import kotlinx.datetime.format
|
|
import kotlinx.datetime.toLocalDateTime
|
|
import kotlinx.serialization.json.Json
|
|
import kotlinx.serialization.json.decodeFromJsonElement
|
|
import ru.fromchat.DATETIME_FORMAT
|
|
import ru.fromchat.R
|
|
import ru.fromchat.api.ApiClient
|
|
import ru.fromchat.api.Message
|
|
import ru.fromchat.api.WebSocketManager
|
|
import ru.fromchat.api.apiRequest
|
|
import kotlin.time.ExperimentalTime
|
|
import kotlin.time.Instant
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalTime::class)
|
|
@Composable
|
|
fun PublicChatScreen() {
|
|
val navController = LocalNavController.current
|
|
val scope = rememberCoroutineScope()
|
|
val scrollState = rememberScrollState()
|
|
|
|
val messages = remember { mutableStateListOf<Message>() }
|
|
|
|
LaunchedEffect(Unit) {
|
|
val response = apiRequest {
|
|
ApiClient.getMessages()
|
|
}
|
|
|
|
if (response.isSuccess) {
|
|
messages.clear()
|
|
messages += response.getOrThrow().messages
|
|
}
|
|
|
|
WebSocketManager.connect()
|
|
WebSocketManager.setGlobalMessageHandler { msg ->
|
|
scope.launch {
|
|
try {
|
|
messages += Json.decodeFromJsonElement<Message>(msg.data!!)
|
|
delay(500)
|
|
scrollState.animateScrollTo(scrollState.maxValue)
|
|
} catch (e: Exception) {
|
|
Log.e("PublicChatScreen", "Failed to process incoming message:", e)
|
|
}
|
|
}
|
|
}
|
|
|
|
delay(500)
|
|
scrollState.animateScrollTo(scrollState.maxValue)
|
|
}
|
|
|
|
Scaffold(
|
|
topBar = {
|
|
TopAppBar(
|
|
title = {
|
|
Text(
|
|
text = stringResource(R.string.public_chat),
|
|
maxLines = 1,
|
|
overflow = TextOverflow.Ellipsis
|
|
)
|
|
},
|
|
navigationIcon = {
|
|
IconButton(
|
|
onClick = {
|
|
navController.navigateUp()
|
|
}
|
|
) {
|
|
Icon(
|
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
|
contentDescription = "Back"
|
|
)
|
|
}
|
|
}
|
|
)
|
|
},
|
|
bottomBar = {
|
|
Row(
|
|
Modifier
|
|
.background(MaterialTheme.colorScheme.surfaceContainer)
|
|
.fillMaxWidth()
|
|
.windowInsetsPadding(
|
|
WindowInsets.safeDrawing.exclude(
|
|
WindowInsets.safeDrawing.only(WindowInsetsSides.Top)
|
|
)
|
|
)
|
|
.padding(vertical = 8.dp, horizontal = 12.dp),
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
val message = rememberTextFieldState()
|
|
|
|
BasicTextField(
|
|
state = message,
|
|
modifier = Modifier.weight(1f),
|
|
textStyle = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onSurface),
|
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
|
lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 4),
|
|
decorator = { innerTextField ->
|
|
Box {
|
|
if (message.text.isEmpty()) {
|
|
Text(
|
|
text = stringResource(R.string.message_placeholder),
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
)
|
|
}
|
|
innerTextField()
|
|
}
|
|
}
|
|
)
|
|
|
|
IconButton(
|
|
onClick = {
|
|
scope.launch {
|
|
val response = apiRequest {
|
|
ApiClient.send(message.text.toString())
|
|
}
|
|
|
|
message.setTextAndPlaceCursorAtEnd("")
|
|
|
|
if (response.isSuccess) {
|
|
messages += response.getOrThrow().message
|
|
delay(500)
|
|
scrollState.animateScrollTo(scrollState.maxValue)
|
|
}
|
|
}
|
|
}
|
|
) {
|
|
Icon(
|
|
imageVector = Icons.AutoMirrored.Filled.Send,
|
|
contentDescription = "Send"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
) { innerPadding ->
|
|
Column(
|
|
Modifier
|
|
.padding(innerPadding)
|
|
.verticalScroll(scrollState)
|
|
) {
|
|
@Composable
|
|
fun Message(
|
|
text: String,
|
|
isAuthor: Boolean,
|
|
isRead: Boolean,
|
|
timestamp: String
|
|
) {
|
|
var rowWidth by remember { mutableStateOf(0.dp) }
|
|
val density = LocalDensity.current
|
|
|
|
Row(
|
|
modifier = Modifier
|
|
.padding(horizontal = 16.dp, vertical = 4.dp)
|
|
.fillMaxWidth()
|
|
.onSizeChanged {
|
|
rowWidth = with (density) {
|
|
it.width.toDp()
|
|
}
|
|
},
|
|
horizontalArrangement = if (isAuthor) Arrangement.End else Arrangement.Start
|
|
) {
|
|
val background =
|
|
if (isAuthor)
|
|
MaterialTheme.colorScheme.primaryContainer
|
|
else MaterialTheme.colorScheme.surfaceContainer
|
|
|
|
Column(
|
|
Modifier
|
|
.clip(
|
|
RoundedCornerShape(
|
|
topStart = if (isAuthor) 16.dp else 5.dp,
|
|
topEnd = 16.dp,
|
|
bottomEnd = if (isAuthor) 5.dp else 16.dp,
|
|
bottomStart = 16.dp
|
|
)
|
|
)
|
|
.background(background)
|
|
.padding(10.dp)
|
|
.width(IntrinsicSize.Max)
|
|
.widthIn(max = rowWidth * 0.80f)
|
|
) {
|
|
Text(text)
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
horizontalArrangement = Arrangement.spacedBy(0.dp, Alignment.End),
|
|
modifier = Modifier.fillMaxWidth()
|
|
) {
|
|
Spacer(Modifier.weight(1f))
|
|
Text(
|
|
text = timestamp,
|
|
fontSize = 14.sp
|
|
)
|
|
if (isRead) {
|
|
Icon(
|
|
imageVector = Icons.Filled.DoneAll,
|
|
contentDescription = "Read",
|
|
modifier = Modifier.size(17.dp)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
messages.forEach {
|
|
Message(
|
|
text = it.content,
|
|
isAuthor = it.username == ApiClient.user!!.username,
|
|
isRead = it.is_read,
|
|
timestamp = Instant
|
|
.parse(it.utcTimestamp)
|
|
.toLocalDateTime(TimeZone.currentSystemDefault())
|
|
.format(DATETIME_FORMAT)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
} |