mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user