diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6353b28..5e2076d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/ru/fromchat/Constants.kt b/app/src/main/java/ru/fromchat/Constants.kt index 058ef50..546788d 100644 --- a/app/src/main/java/ru/fromchat/Constants.kt +++ b/app/src/main/java/ru/fromchat/Constants.kt @@ -1,3 +1,20 @@ package ru.fromchat -const val API_HOST = "fromchat.ru" \ No newline at end of file +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() +} \ No newline at end of file diff --git a/app/src/main/java/ru/fromchat/api/Models.kt b/app/src/main/java/ru/fromchat/api/Models.kt index 7e17356..5d92406 100644 --- a/app/src/main/java/ru/fromchat/api/Models.kt +++ b/app/src/main/java/ru/fromchat/api/Models.kt @@ -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( diff --git a/app/src/main/java/ru/fromchat/ui/chat/BaseChat.kt b/app/src/main/java/ru/fromchat/ui/chat/BaseChat.kt deleted file mode 100644 index a00402a..0000000 --- a/app/src/main/java/ru/fromchat/ui/chat/BaseChat.kt +++ /dev/null @@ -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() } - 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("Отправить") } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/fromchat/ui/chat/ChatScreen.kt b/app/src/main/java/ru/fromchat/ui/chat/ChatScreen.kt index 66ab4f1..803923d 100644 --- a/app/src/main/java/ru/fromchat/ui/chat/ChatScreen.kt +++ b/app/src/main/java/ru/fromchat/ui/chat/ChatScreen.kt @@ -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)) } } diff --git a/app/src/main/java/ru/fromchat/ui/chat/PublicChatScreen.kt b/app/src/main/java/ru/fromchat/ui/chat/PublicChatScreen.kt index f519932..f68c4fd 100644 --- a/app/src/main/java/ru/fromchat/ui/chat/PublicChatScreen.kt +++ b/app/src/main/java/ru/fromchat/ui/chat/PublicChatScreen.kt @@ -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) ) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d7658e4..669865e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }