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.material3)
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.material.icons.extended) implementation(libs.androidx.material.icons.extended)
implementation(libs.kotlinx.datetime)
implementation(libs.ktor.client.core) implementation(libs.ktor.client.core)
implementation(libs.ktor.client.okhttp) implementation(libs.ktor.client.okhttp)
@@ -1,3 +1,20 @@
package ru.fromchat package ru.fromchat
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.format.char
const val API_HOST = "fromchat.ru" 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 username: String,
val profile_picture: String?, val profile_picture: String?,
val reply_to: Message? val reply_to: Message?
) ) {
val utcTimestamp = "${timestamp}Z"
}
@Serializable @Serializable
data class SendMessageRequest( 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.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import ru.fromchat.R import ru.fromchat.R
import ru.fromchat.ui.LocalNavController import ru.fromchat.ui.LocalNavController
enum class ChatTab { CHATS, CONTACTS, DMS }
@Composable @Composable
fun ChatScreen() { fun ChatScreen() {
var selectedTab by remember { mutableStateOf(ChatTab.CHATS) } var selectedTab by remember { mutableStateOf("chats") }
val navController = LocalNavController.current val navController = LocalNavController.current
Scaffold( Scaffold(
bottomBar = { bottomBar = {
NavigationBar { NavigationBar {
NavigationBarItem( NavigationBarItem(
selected = selectedTab == ChatTab.CHATS, selected = selectedTab == "chats",
onClick = { selectedTab = ChatTab.CHATS }, onClick = { selectedTab = "chats" },
label = { Text(stringResource(R.string.chats)) }, label = { Text(stringResource(R.string.chats)) },
icon = { Icon(Icons.AutoMirrored.Filled.Chat, contentDescription = null) } icon = { Icon(Icons.AutoMirrored.Filled.Chat, contentDescription = null) }
) )
NavigationBarItem( NavigationBarItem(
selected = selectedTab == ChatTab.CONTACTS, selected = selectedTab == "contacts",
onClick = { selectedTab = ChatTab.CONTACTS }, onClick = { selectedTab = "contacts" },
label = { Text(stringResource(R.string.contacts)) }, label = { Text(stringResource(R.string.contacts)) },
icon = { Icon(Icons.Filled.Contacts, contentDescription = null) } icon = { Icon(Icons.Filled.Contacts, contentDescription = null) }
) )
NavigationBarItem( NavigationBarItem(
selected = selectedTab == ChatTab.DMS, selected = selectedTab == "dms",
onClick = { selectedTab = ChatTab.DMS }, onClick = { selectedTab = "dms" },
label = { Text(stringResource(R.string.dms)) }, label = { Text(stringResource(R.string.dms)) },
icon = { Icon(Icons.Filled.Mail, contentDescription = null) } icon = { Icon(Icons.Filled.Mail, contentDescription = null) }
) )
} }
} }
) { innerPadding -> ) { innerPadding ->
Column(modifier = Modifier.fillMaxSize().padding(innerPadding).padding(12.dp)) { Column(
Modifier
.fillMaxSize()
.padding(innerPadding)
) {
when (selectedTab) { when (selectedTab) {
ChatTab.CHATS -> { "chats" -> {
ListItem( ListItem(
headlineContent = { Text(stringResource(R.string.public_chat)) }, headlineContent = { Text(stringResource(R.string.public_chat)) },
supportingContent = { Text(stringResource(R.string.chat_last_mesaage)) }, supportingContent = { Text(stringResource(R.string.chat_last_mesaage)) },
@@ -67,10 +68,10 @@ fun ChatScreen() {
} }
) )
} }
ChatTab.CONTACTS -> { "contacts" -> {
Text(stringResource(R.string.coming_soon)) Text(stringResource(R.string.coming_soon))
} }
ChatTab.DMS -> { "dms" -> {
Text(stringResource(R.string.coming_soon)) Text(stringResource(R.string.coming_soon))
} }
} }
@@ -1,7 +1,6 @@
package ru.fromchat.ui.chat package ru.fromchat.ui.chat
import android.util.Log import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -41,6 +40,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -58,16 +58,22 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch 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.Json
import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.decodeFromJsonElement
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.WebSocketManager import ru.fromchat.api.WebSocketManager
import ru.fromchat.api.apiRequest import ru.fromchat.api.apiRequest
import ru.fromchat.ui.LocalNavController import ru.fromchat.ui.LocalNavController
import kotlin.time.ExperimentalTime
import kotlin.time.Instant
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalTime::class)
@Composable @Composable
fun PublicChatScreen() { fun PublicChatScreen() {
val navController = LocalNavController.current val navController = LocalNavController.current
@@ -221,10 +227,10 @@ fun PublicChatScreen() {
Modifier Modifier
.clip( .clip(
RoundedCornerShape( RoundedCornerShape(
if (isAuthor) 16.dp else 5.dp, topStart = if (isAuthor) 16.dp else 5.dp,
16.dp, topEnd = 16.dp,
if (isAuthor) 5.dp else 16.dp, bottomEnd = if (isAuthor) 5.dp else 16.dp,
16.dp bottomStart = 16.dp
) )
) )
.background(background) .background(background)
@@ -260,7 +266,10 @@ fun PublicChatScreen() {
text = it.content, text = it.content,
isAuthor = it.username == ApiClient.user!!.username, isAuthor = it.username == ApiClient.user!!.username,
isRead = it.is_read, 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] [versions]
agp = "8.12.2" agp = "8.12.3"
kotlin = "2.2.10" kotlin = "2.2.10"
coreKtx = "1.17.0" coreKtx = "1.17.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.3.0" junitVersion = "1.3.0"
espressoCore = "3.7.0" espressoCore = "3.7.0"
kotlinxDatetime = "0.7.1"
lifecycleRuntimeKtx = "2.9.3" lifecycleRuntimeKtx = "2.9.3"
activityCompose = "1.10.1" activityCompose = "1.11.0"
composeBom = "2025.08.01" composeBom = "2025.09.00"
materialIconsExtended = "1.7.8" materialIconsExtended = "1.7.8"
navigationCompose = "2.9.3" navigationCompose = "2.9.4"
ktor = "3.2.3" ktor = "3.2.3"
kotlinxSerializationJson = "1.9.0" 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-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } 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-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-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" }
ktor-client-websockets = { group = "io.ktor", name = "ktor-client-websockets", version.ref = "ktor" } ktor-client-websockets = { group = "io.ktor", name = "ktor-client-websockets", version.ref = "ktor" }