mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
Make the base
This commit is contained in:
@@ -10,7 +10,8 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.FromChat">
|
||||
android:theme="@style/Theme.FromChat"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package ru.fromchat.api
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.logging.LogLevel
|
||||
import io.ktor.client.plugins.logging.Logger
|
||||
import io.ktor.client.plugins.logging.Logging
|
||||
import io.ktor.client.plugins.websocket.WebSockets
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.URLBuilder
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.fromchat.API_HOST
|
||||
|
||||
object ApiClient {
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
|
||||
val http = HttpClient {
|
||||
install(ContentNegotiation) {
|
||||
json(json)
|
||||
}
|
||||
|
||||
install(Logging) {
|
||||
level = LogLevel.INFO
|
||||
logger = object : Logger {
|
||||
override fun log(message: String) {
|
||||
// Replace with Logcat if needed
|
||||
println(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
install(WebSockets)
|
||||
}
|
||||
|
||||
@Volatile
|
||||
var token: String? = null
|
||||
private set
|
||||
|
||||
private fun apiUrlBuilder(): URLBuilder {
|
||||
return URLBuilder().apply {
|
||||
protocol = io.ktor.http.URLProtocol.HTTPS
|
||||
host = API_HOST
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun login(request: LoginRequest): LoginResponse {
|
||||
// Endpoint exists in web: POST /login with {username,password}
|
||||
val response = http.post("https://fromchat.ru/api/login") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}.body<LoginResponse>()
|
||||
token = response.token
|
||||
return response
|
||||
}
|
||||
|
||||
suspend fun register(request: RegisterRequest) {
|
||||
// Endpoint exists in web: POST /register with {username,password,confirm_password}
|
||||
http.post("https://fromchat.ru/api/register") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package ru.fromchat.api
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class LoginRequest(
|
||||
val username: String,
|
||||
val password: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class RegisterRequest(
|
||||
val username: String,
|
||||
val password: String,
|
||||
val confirm_password: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ErrorResponse(
|
||||
val message: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class User(
|
||||
val id: Int,
|
||||
val created_at: String,
|
||||
val last_seen: String,
|
||||
val online: Boolean,
|
||||
val username: String,
|
||||
val admin: Boolean? = null,
|
||||
val bio: String? = null,
|
||||
val profile_picture: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LoginResponse(
|
||||
val user: User,
|
||||
val token: String
|
||||
)
|
||||
|
||||
// WebSocket types mirror frontend src/core/types.d.ts
|
||||
@Serializable
|
||||
data class WebSocketCredentials(
|
||||
val scheme: String,
|
||||
val credentials: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class WebSocketError(
|
||||
val code: Int,
|
||||
val detail: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class WebSocketMessage(
|
||||
val type: String,
|
||||
val credentials: WebSocketCredentials? = null,
|
||||
val data: kotlinx.serialization.json.JsonElement? = null,
|
||||
val error: WebSocketError? = null
|
||||
)
|
||||
@@ -0,0 +1,123 @@
|
||||
package ru.fromchat.api
|
||||
|
||||
import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession
|
||||
import io.ktor.client.plugins.websocket.webSocket
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.URLProtocol
|
||||
import io.ktor.http.encodedPath
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.readText
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.fromchat.API_HOST
|
||||
|
||||
object WebSocketManager {
|
||||
// Config
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
private val _messages = MutableSharedFlow<WebSocketMessage>(replay = 0, extraBufferCapacity = 64)
|
||||
val messages = _messages.asSharedFlow()
|
||||
|
||||
@Volatile
|
||||
private var globalHandler: ((WebSocketMessage) -> Unit)? = null
|
||||
|
||||
fun setGlobalMessageHandler(handler: ((WebSocketMessage) -> Unit)?) {
|
||||
globalHandler = handler
|
||||
}
|
||||
|
||||
// State
|
||||
@Volatile private var connecting: Boolean = false
|
||||
@Volatile private var session: DefaultClientWebSocketSession? = null
|
||||
|
||||
fun connect() {
|
||||
if (connecting) return
|
||||
connecting = true
|
||||
|
||||
scope.launch {
|
||||
while (isActive) {
|
||||
try {
|
||||
ApiClient.http.webSocket(
|
||||
method = HttpMethod.Get,
|
||||
host = API_HOST,
|
||||
request = {
|
||||
url {
|
||||
protocol = URLProtocol.WSS
|
||||
host = "fromchat.ru"
|
||||
encodedPath = "/api/chat/ws"
|
||||
}
|
||||
}
|
||||
) {
|
||||
session = this
|
||||
|
||||
for (frame in incoming) {
|
||||
val text = (frame as? Frame.Text)?.readText() ?: continue
|
||||
try {
|
||||
val msg = json.decodeFromString(WebSocketMessage.serializer(), text)
|
||||
globalHandler?.invoke(msg)
|
||||
_messages.emit(msg)
|
||||
} catch (_: Throwable) {
|
||||
// ignore malformed
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
delay(3000)
|
||||
} finally {
|
||||
session = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun send(message: WebSocketMessage) {
|
||||
val payload = json.encodeToString(WebSocketMessage.serializer(), message)
|
||||
ApiClient.http.webSocket(
|
||||
method = HttpMethod.Get,
|
||||
host = API_HOST,
|
||||
request = {
|
||||
url {
|
||||
protocol = URLProtocol.WSS
|
||||
host = "fromchat.ru"
|
||||
encodedPath = "/api/chat/ws"
|
||||
}
|
||||
}
|
||||
) {
|
||||
send(Frame.Text(payload))
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
suspend fun request(message: WebSocketMessage, timeoutMs: Long = 10_000): WebSocketMessage? {
|
||||
val ch = Channel<WebSocketMessage>(capacity = 1)
|
||||
val handler: (WebSocketMessage) -> Unit = {
|
||||
ch.trySend(it)
|
||||
}
|
||||
setGlobalMessageHandler(handler)
|
||||
|
||||
return try {
|
||||
send(message)
|
||||
withTimeout(timeoutMs) { ch.receive() }
|
||||
} catch (_: TimeoutCancellationException) {
|
||||
null
|
||||
} finally {
|
||||
setGlobalMessageHandler(null)
|
||||
ch.close()
|
||||
}
|
||||
}
|
||||
|
||||
fun shutdown() {
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,23 @@
|
||||
package ru.fromchat.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import ru.fromchat.ui.auth.LoginScreen
|
||||
import ru.fromchat.ui.auth.RegisterScreen
|
||||
import ru.fromchat.ui.chat.ChatScreen
|
||||
|
||||
@Composable
|
||||
fun App() {
|
||||
FromChatTheme {
|
||||
|
||||
val (route, setRoute) = remember { mutableStateOf("login") }
|
||||
when (route) {
|
||||
"login" -> LoginScreen(
|
||||
onLoginSuccess = { setRoute("chat") },
|
||||
onNavigateToRegister = { setRoute("register") }
|
||||
)
|
||||
"register" -> RegisterScreen(onRegistered = { setRoute("login") })
|
||||
"chat" -> ChatScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package ru.fromchat.ui.auth
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Login
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.fromchat.api.ApiClient
|
||||
import ru.fromchat.api.LoginRequest
|
||||
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
onLoginSuccess: () -> Unit,
|
||||
onNavigateToRegister: () -> Unit
|
||||
) {
|
||||
var username by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var alert by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Card(modifier = Modifier.padding(16.dp)) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
RowHeader()
|
||||
|
||||
if (alert != null) {
|
||||
Text(text = alert!!, color = MaterialTheme.colorScheme.error)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text("Имя пользователя") },
|
||||
singleLine = true
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text("Пароль") },
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation()
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Button(onClick = {
|
||||
if (username.isBlank() || password.isBlank()) {
|
||||
alert = "Пожалуйста, заполните все поля"
|
||||
return@Button
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
ApiClient.login(LoginRequest(username, password))
|
||||
onLoginSuccess()
|
||||
} catch (e: Exception) {
|
||||
alert = "Неверное имя пользователя или пароль"
|
||||
}
|
||||
}
|
||||
}) { Text("Войти") }
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Button(onClick = onNavigateToRegister) { Text("Зарегистрируйтесь") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowHeader() {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(Icons.AutoMirrored.Filled.Login, contentDescription = null)
|
||||
Text("Добро пожаловать!")
|
||||
Text("Войдите в свой аккаунт", style = MaterialTheme.typography.bodyMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package ru.fromchat.ui.auth
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PersonAdd
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.fromchat.api.ApiClient
|
||||
import ru.fromchat.api.RegisterRequest
|
||||
|
||||
@Composable
|
||||
fun RegisterScreen(
|
||||
onRegistered: () -> Unit
|
||||
) {
|
||||
var username by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var confirmPassword by remember { mutableStateOf("") }
|
||||
var alert by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Card(modifier = Modifier.padding(16.dp)) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(Icons.Default.PersonAdd, contentDescription = null)
|
||||
Text("Регистрация")
|
||||
Text("Создайте новый аккаунт", style = MaterialTheme.typography.bodyMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
if (alert != null) {
|
||||
Text(text = alert!!, color = MaterialTheme.colorScheme.error)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text("Имя пользователя") },
|
||||
singleLine = true
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text("Пароль") },
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation()
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = confirmPassword,
|
||||
onValueChange = { confirmPassword = it },
|
||||
label = { Text("Подтвердите пароль") },
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation()
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Button(onClick = {
|
||||
if (username.isBlank() || password.isBlank() || confirmPassword.isBlank()) {
|
||||
alert = "Пожалуйста, заполните все поля"
|
||||
return@Button
|
||||
}
|
||||
if (password != confirmPassword) {
|
||||
alert = "Пароли не совпадают"
|
||||
return@Button
|
||||
}
|
||||
if (username.length !in 3..20) {
|
||||
alert = "Имя пользователя должно быть от 3 до 20 символов"
|
||||
return@Button
|
||||
}
|
||||
if (password.length !in 5..50) {
|
||||
alert = "Пароль должен быть от 5 до 50 символов"
|
||||
return@Button
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
ApiClient.register(RegisterRequest(username, password, confirmPassword))
|
||||
onRegistered()
|
||||
} catch (e: Exception) {
|
||||
alert = "Ошибка при регистрации"
|
||||
}
|
||||
}
|
||||
}) { Text("Зарегистрироваться") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
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.
|
||||
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("Отправить") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package ru.fromchat.ui.chat
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Chat
|
||||
import androidx.compose.material.icons.filled.Contacts
|
||||
import androidx.compose.material.icons.filled.Mail
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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
|
||||
|
||||
enum class ChatTab { CHATS, CONTACTS, DMS }
|
||||
|
||||
@Composable
|
||||
fun ChatScreen() {
|
||||
var selectedTab by remember { mutableStateOf(ChatTab.CHATS) }
|
||||
var activeChat by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
NavigationBar {
|
||||
NavigationBarItem(
|
||||
selected = selectedTab == ChatTab.CHATS,
|
||||
onClick = { selectedTab = ChatTab.CHATS },
|
||||
label = { Text("Чаты") },
|
||||
icon = { Icon(Icons.AutoMirrored.Filled.Chat, contentDescription = null) }
|
||||
)
|
||||
NavigationBarItem(
|
||||
selected = selectedTab == ChatTab.CONTACTS,
|
||||
onClick = { selectedTab = ChatTab.CONTACTS },
|
||||
label = { Text("Контакты") },
|
||||
icon = { Icon(Icons.Filled.Contacts, contentDescription = null) }
|
||||
)
|
||||
NavigationBarItem(
|
||||
selected = selectedTab == ChatTab.DMS,
|
||||
onClick = { selectedTab = ChatTab.DMS },
|
||||
label = { Text("ЛС") },
|
||||
icon = { Icon(Icons.Filled.Mail, contentDescription = null) }
|
||||
)
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(modifier = Modifier.fillMaxSize().padding(paddingValues).padding(12.dp)) {
|
||||
when (selectedTab) {
|
||||
ChatTab.CHATS -> {
|
||||
if (activeChat == null) {
|
||||
Text("Общий чат", modifier = Modifier.clickable { activeChat = "Общий чат" }.padding(8.dp))
|
||||
Text("Общий чат 2", modifier = Modifier.clickable { activeChat = "Общий чат 2" }.padding(8.dp))
|
||||
} else {
|
||||
BaseChat(chatTitle = activeChat!!)
|
||||
}
|
||||
}
|
||||
ChatTab.CONTACTS -> {
|
||||
Text("Скоро будет...")
|
||||
}
|
||||
ChatTab.DMS -> {
|
||||
Text("Скоро будет...")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user