Improve the UI

Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
This commit is contained in:
2025-09-11 19:22:29 +03:00
Unverified
parent 84939c4286
commit 582160e88d
7 changed files with 59 additions and 114 deletions
+1
View File
@@ -77,6 +77,7 @@ dependencies {
implementation(libs.androidx.material3)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.material.icons.extended)
implementation(libs.kotlinx.datetime)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.okhttp)
@@ -1,3 +1,20 @@
package ru.fromchat
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.format.char
const val API_HOST = "fromchat.ru"
val DATETIME_FORMAT = LocalDateTime.Format {
day()
char('.')
monthNumber()
char('.')
year()
char(' ')
hour()
char(':')
minute()
}
+3 -1
View File
@@ -55,7 +55,9 @@ data class Message(
val username: String,
val profile_picture: String?,
val reply_to: Message?
)
) {
val utcTimestamp = "${timestamp}Z"
}
@Serializable
data class SendMessageRequest(
@@ -1,87 +0,0 @@
package ru.fromchat.ui.chat
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
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.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import ru.fromchat.api.WebSocketManager
import ru.fromchat.api.WebSocketMessage
data class ChatMessage(val author: String, val content: String)
@OptIn(DelicateCoroutinesApi::class)
@Composable
fun BaseChat(
chatTitle: String
) {
val messages = remember { mutableStateListOf<ChatMessage>() }
var composing by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
WebSocketManager.connect()
WebSocketManager.setGlobalMessageHandler { msg ->
// Placeholder: append raw data content if present
val text = msg.data?.toString() ?: return@setGlobalMessageHandler
messages.add(ChatMessage(author = "Server", content = text))
}
}
Surface(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier.fillMaxSize().padding(8.dp)) {
Text(chatTitle)
LazyColumn(modifier = Modifier.weight(1f)) {
items(messages) { m ->
Text("${m.author}: ${m.content}")
}
}
Row {
OutlinedTextField(
value = composing,
onValueChange = { composing = it },
modifier = Modifier.weight(1f),
label = { Text("Сообщение") }
)
Button(onClick = {
if (composing.isNotBlank()) {
// Note: server schema for public chat messages is not specified in the repo.
// We send a generic WebSocketMessage to match frontend type contract.
// TODO correct the payload
val payload = WebSocketMessage(
type = "public_chat_message",
data = buildJsonObject {
put("content", JsonPrimitive(composing))
put("chatName", JsonPrimitive(chatTitle))
}
)
// Fire and forget
GlobalScope.launch { WebSocketManager.send(payload) }
messages.add(ChatMessage(author = "Вы", content = composing))
composing = ""
}
}) { Text("Отправить") }
}
}
}
}
@@ -21,44 +21,45 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import ru.fromchat.R
import ru.fromchat.ui.LocalNavController
enum class ChatTab { CHATS, CONTACTS, DMS }
@Composable
fun ChatScreen() {
var selectedTab by remember { mutableStateOf(ChatTab.CHATS) }
var selectedTab by remember { mutableStateOf("chats") }
val navController = LocalNavController.current
Scaffold(
bottomBar = {
NavigationBar {
NavigationBarItem(
selected = selectedTab == ChatTab.CHATS,
onClick = { selectedTab = ChatTab.CHATS },
selected = selectedTab == "chats",
onClick = { selectedTab = "chats" },
label = { Text(stringResource(R.string.chats)) },
icon = { Icon(Icons.AutoMirrored.Filled.Chat, contentDescription = null) }
)
NavigationBarItem(
selected = selectedTab == ChatTab.CONTACTS,
onClick = { selectedTab = ChatTab.CONTACTS },
selected = selectedTab == "contacts",
onClick = { selectedTab = "contacts" },
label = { Text(stringResource(R.string.contacts)) },
icon = { Icon(Icons.Filled.Contacts, contentDescription = null) }
)
NavigationBarItem(
selected = selectedTab == ChatTab.DMS,
onClick = { selectedTab = ChatTab.DMS },
selected = selectedTab == "dms",
onClick = { selectedTab = "dms" },
label = { Text(stringResource(R.string.dms)) },
icon = { Icon(Icons.Filled.Mail, contentDescription = null) }
)
}
}
) { innerPadding ->
Column(modifier = Modifier.fillMaxSize().padding(innerPadding).padding(12.dp)) {
Column(
Modifier
.fillMaxSize()
.padding(innerPadding)
) {
when (selectedTab) {
ChatTab.CHATS -> {
"chats" -> {
ListItem(
headlineContent = { Text(stringResource(R.string.public_chat)) },
supportingContent = { Text(stringResource(R.string.chat_last_mesaage)) },
@@ -67,10 +68,10 @@ fun ChatScreen() {
}
)
}
ChatTab.CONTACTS -> {
"contacts" -> {
Text(stringResource(R.string.coming_soon))
}
ChatTab.DMS -> {
"dms" -> {
Text(stringResource(R.string.coming_soon))
}
}
@@ -1,7 +1,6 @@
package ru.fromchat.ui.chat
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -41,6 +40,7 @@ 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
@@ -58,16 +58,22 @@ 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 ru.fromchat.ui.LocalNavController
import kotlin.time.ExperimentalTime
import kotlin.time.Instant
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalTime::class)
@Composable
fun PublicChatScreen() {
val navController = LocalNavController.current
@@ -221,10 +227,10 @@ fun PublicChatScreen() {
Modifier
.clip(
RoundedCornerShape(
if (isAuthor) 16.dp else 5.dp,
16.dp,
if (isAuthor) 5.dp else 16.dp,
16.dp
topStart = if (isAuthor) 16.dp else 5.dp,
topEnd = 16.dp,
bottomEnd = if (isAuthor) 5.dp else 16.dp,
bottomStart = 16.dp
)
)
.background(background)
@@ -260,7 +266,10 @@ fun PublicChatScreen() {
text = it.content,
isAuthor = it.username == ApiClient.user!!.username,
isRead = it.is_read,
timestamp = it.timestamp
timestamp = Instant
.parse(it.utcTimestamp)
.toLocalDateTime(TimeZone.currentSystemDefault())
.format(DATETIME_FORMAT)
)
}
}
+6 -4
View File
@@ -1,15 +1,16 @@
[versions]
agp = "8.12.2"
agp = "8.12.3"
kotlin = "2.2.10"
coreKtx = "1.17.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
kotlinxDatetime = "0.7.1"
lifecycleRuntimeKtx = "2.9.3"
activityCompose = "1.10.1"
composeBom = "2025.08.01"
activityCompose = "1.11.0"
composeBom = "2025.09.00"
materialIconsExtended = "1.7.8"
navigationCompose = "2.9.3"
navigationCompose = "2.9.4"
ktor = "3.2.3"
kotlinxSerializationJson = "1.9.0"
@@ -30,6 +31,7 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" }
ktor-client-websockets = { group = "io.ktor", name = "ktor-client-websockets", version.ref = "ktor" }