mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
Implement proper iOS structure
This commit is contained in:
@@ -5,6 +5,7 @@ import io.ktor.client.call.body
|
||||
import io.ktor.client.plugins.ClientRequestException
|
||||
|
||||
suspend inline fun <Response> apiRequest(
|
||||
unexpectedError: String,
|
||||
onError: (String, Exception) -> Unit = { _, _ -> },
|
||||
onSuccess: (Response) -> Unit = {},
|
||||
request: suspend () -> Response
|
||||
@@ -17,7 +18,7 @@ suspend inline fun <Response> apiRequest(
|
||||
val message = if (e.response.status.value in arrayOf(401, 403)) {
|
||||
e.response.body<ErrorResponse>().detail
|
||||
} else {
|
||||
"Unexpected error"
|
||||
unexpectedError
|
||||
}
|
||||
|
||||
Logger.e("API", "API request failed: $message", e)
|
||||
@@ -26,7 +27,7 @@ suspend inline fun <Response> apiRequest(
|
||||
return Result.failure(e)
|
||||
} catch (e: Exception) {
|
||||
Logger.e("API", "API request failed: ${e.message}", e)
|
||||
onError("Unexpected error", e)
|
||||
onError(unexpectedError, e)
|
||||
return Result.failure(e)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package ru.fromchat.api
|
||||
|
||||
import ru.fromchat.core.Logger
|
||||
import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession
|
||||
import io.ktor.client.plugins.websocket.webSocket
|
||||
import io.ktor.client.request.url
|
||||
@@ -10,6 +9,7 @@ import io.ktor.websocket.readText
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -20,6 +20,8 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.fromchat.WS_API_HOST
|
||||
import ru.fromchat.core.Logger
|
||||
import kotlin.concurrent.Volatile
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
object WebSocketManager {
|
||||
@@ -41,7 +43,8 @@ object WebSocketManager {
|
||||
}
|
||||
|
||||
// State
|
||||
@Volatile private var connecting: Boolean = false
|
||||
@Volatile
|
||||
private var connecting: Boolean = false
|
||||
@Volatile private var session: DefaultClientWebSocketSession? = null
|
||||
|
||||
fun connect() {
|
||||
|
||||
@@ -6,3 +6,4 @@ expect object Logger {
|
||||
fun w(tag: String, message: String, throwable: Throwable? = null)
|
||||
fun e(tag: String, message: String, throwable: Throwable? = null)
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,10 @@ fun App() {
|
||||
) {
|
||||
composable("login") {
|
||||
LoginScreen(
|
||||
onLoginSuccess = { navController.navigate("chat") },
|
||||
onLoginSuccess = {
|
||||
navController.navigate("chat")
|
||||
navController.popBackStack()
|
||||
},
|
||||
onNavigateToRegister = { navController.navigate("register") }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,22 +24,32 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.pr0gramm3r101.utils.crypto.deriveAuthSecret
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.fromchat.R
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import ru.fromchat.Res
|
||||
import ru.fromchat.api.ApiClient
|
||||
import ru.fromchat.api.LoginRequest
|
||||
import ru.fromchat.api.apiRequest
|
||||
import ru.fromchat.error_unexpected
|
||||
import ru.fromchat.fill_all_fields
|
||||
import ru.fromchat.login
|
||||
import ru.fromchat.login_d
|
||||
import ru.fromchat.password
|
||||
import ru.fromchat.register_button
|
||||
import ru.fromchat.ui.RowHeader
|
||||
import com.pr0gramm3r101.utils.crypto.deriveAuthSecret
|
||||
import ru.fromchat.username
|
||||
import ru.fromchat.welcome
|
||||
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
onLoginSuccess: () -> Unit,
|
||||
onNavigateToRegister: () -> Unit
|
||||
) {
|
||||
val errorUnexpected = stringResource(Res.string.error_unexpected)
|
||||
|
||||
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
|
||||
var username by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
@@ -63,8 +73,8 @@ fun LoginScreen(
|
||||
) {
|
||||
RowHeader(
|
||||
icon = Icons.AutoMirrored.Filled.Login,
|
||||
title = stringResource(R.string.welcome),
|
||||
subtitle = stringResource(R.string.login_d)
|
||||
title = stringResource(Res.string.welcome),
|
||||
subtitle = stringResource(Res.string.login_d)
|
||||
)
|
||||
|
||||
if (alert != null) {
|
||||
@@ -74,25 +84,25 @@ fun LoginScreen(
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text(stringResource(R.string.username)) },
|
||||
label = { Text(stringResource(Res.string.username)) },
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text(stringResource(R.string.password)) },
|
||||
label = { Text(stringResource(Res.string.password)) },
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation()
|
||||
)
|
||||
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
val alert_error_filling = stringResource(R.string.fill_all_fields)
|
||||
val alertErrorFilling = stringResource(Res.string.fill_all_fields)
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
if (username.isBlank() || password.isBlank()) {
|
||||
alert = alert_error_filling
|
||||
alert = alertErrorFilling
|
||||
return@Button
|
||||
}
|
||||
|
||||
@@ -101,6 +111,7 @@ fun LoginScreen(
|
||||
val derived = deriveAuthSecret(username.trim(), password.trim())
|
||||
|
||||
apiRequest(
|
||||
unexpectedError = errorUnexpected,
|
||||
onError = { message, _ ->
|
||||
alert = message
|
||||
},
|
||||
@@ -111,11 +122,11 @@ fun LoginScreen(
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.login))
|
||||
Text(stringResource(Res.string.login))
|
||||
}
|
||||
|
||||
Button(onClick = onNavigateToRegister) {
|
||||
Text(stringResource(R.string.register_button))
|
||||
Text(stringResource(Res.string.register_button))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,22 +27,38 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.pr0gramm3r101.utils.crypto.deriveAuthSecret
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.fromchat.R
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import ru.fromchat.Res
|
||||
import ru.fromchat.api.ApiClient
|
||||
import ru.fromchat.api.RegisterRequest
|
||||
import ru.fromchat.api.apiRequest
|
||||
import ru.fromchat.back
|
||||
import ru.fromchat.confirm_password
|
||||
import ru.fromchat.display_name
|
||||
import ru.fromchat.display_name_error
|
||||
import ru.fromchat.error_unexpected
|
||||
import ru.fromchat.fill_all_fields
|
||||
import ru.fromchat.password
|
||||
import ru.fromchat.password_length_error
|
||||
import ru.fromchat.passwords_dont_match
|
||||
import ru.fromchat.register
|
||||
import ru.fromchat.register_button
|
||||
import ru.fromchat.register_d
|
||||
import ru.fromchat.ui.LocalNavController
|
||||
import ru.fromchat.ui.RowHeader
|
||||
import com.pr0gramm3r101.utils.crypto.deriveAuthSecret
|
||||
import ru.fromchat.username
|
||||
import ru.fromchat.username_length_error
|
||||
|
||||
@Composable
|
||||
fun RegisterScreen(
|
||||
onRegistered: () -> Unit
|
||||
) {
|
||||
val errorUnexpected = stringResource(Res.string.error_unexpected)
|
||||
|
||||
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
|
||||
var username by remember { mutableStateOf("") }
|
||||
var displayName by remember { mutableStateOf("") }
|
||||
@@ -69,8 +85,8 @@ fun RegisterScreen(
|
||||
) {
|
||||
RowHeader(
|
||||
icon = Icons.Filled.PersonAdd,
|
||||
title = stringResource(R.string.register),
|
||||
subtitle = stringResource(R.string.register_d)
|
||||
title = stringResource(Res.string.register),
|
||||
subtitle = stringResource(Res.string.register_d)
|
||||
)
|
||||
|
||||
if (alert != null) {
|
||||
@@ -80,21 +96,21 @@ fun RegisterScreen(
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text(stringResource(R.string.username)) },
|
||||
label = { Text(stringResource(Res.string.username)) },
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = displayName,
|
||||
onValueChange = { displayName = it },
|
||||
label = { Text("Display Name") },
|
||||
label = { Text(stringResource(Res.string.display_name)) },
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text(stringResource(R.string.password)) },
|
||||
label = { Text(stringResource(Res.string.password)) },
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation()
|
||||
)
|
||||
@@ -102,23 +118,24 @@ fun RegisterScreen(
|
||||
OutlinedTextField(
|
||||
value = confirmPassword,
|
||||
onValueChange = { confirmPassword = it },
|
||||
label = { Text(stringResource(R.string.confirm_password)) },
|
||||
label = { Text(stringResource(Res.string.confirm_password)) },
|
||||
singleLine = true,
|
||||
visualTransformation = PasswordVisualTransformation()
|
||||
)
|
||||
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
val alert_error_filling = stringResource(R.string.fill_all_fields)
|
||||
val alert_error_password_little = stringResource(R.string.password_length_error)
|
||||
val alert_error_name_little = stringResource(R.string.username_length_error)
|
||||
val alert_error_password_confrim = stringResource(R.string.passwords_dont_match)
|
||||
val alertErrorFilling = stringResource(Res.string.fill_all_fields)
|
||||
val alertErrorPasswordLittle = stringResource(Res.string.password_length_error)
|
||||
val alertErrorNameLittle = stringResource(Res.string.username_length_error)
|
||||
val alertErrorPasswordConfrim = stringResource(Res.string.passwords_dont_match)
|
||||
val alertErrorDisplayName = stringResource(Res.string.display_name_error)
|
||||
|
||||
IconButton(
|
||||
onClick = { navController.navigateUp() }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back"
|
||||
contentDescription = stringResource(Res.string.back)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -126,23 +143,23 @@ fun RegisterScreen(
|
||||
onClick = {
|
||||
// Checks
|
||||
if (username.isBlank() || displayName.isBlank() || password.isBlank() || confirmPassword.isBlank()) {
|
||||
alert = alert_error_filling
|
||||
alert = alertErrorFilling
|
||||
return@Button
|
||||
}
|
||||
if (password != confirmPassword) {
|
||||
alert = alert_error_password_confrim
|
||||
alert = alertErrorPasswordConfrim
|
||||
return@Button
|
||||
}
|
||||
if (username.length !in 3..20) {
|
||||
alert = alert_error_name_little
|
||||
alert = alertErrorNameLittle
|
||||
return@Button
|
||||
}
|
||||
if (displayName.isBlank() || displayName.length > 64) {
|
||||
alert = "Display name must be between 1 and 64 characters"
|
||||
alert = alertErrorDisplayName
|
||||
return@Button
|
||||
}
|
||||
if (password.length !in 5..50) {
|
||||
alert = alert_error_password_little
|
||||
alert = alertErrorPasswordLittle
|
||||
return@Button
|
||||
}
|
||||
|
||||
@@ -151,6 +168,7 @@ fun RegisterScreen(
|
||||
val derived = deriveAuthSecret(username.trim(), password)
|
||||
|
||||
apiRequest(
|
||||
unexpectedError = errorUnexpected,
|
||||
onError = { message, _ ->
|
||||
alert = message
|
||||
},
|
||||
@@ -168,7 +186,7 @@ fun RegisterScreen(
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.register_button))
|
||||
Text(stringResource(Res.string.register_button))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,12 @@ import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import ru.fromchat.R
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import ru.fromchat.Res
|
||||
import ru.fromchat.chat_last_mesaage
|
||||
import ru.fromchat.chats
|
||||
import ru.fromchat.public_chat
|
||||
import ru.fromchat.ui.LocalNavController
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -28,7 +31,7 @@ fun ChatsTab() {
|
||||
MediumTopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.chats),
|
||||
text = stringResource(Res.string.chats),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
@@ -40,8 +43,8 @@ fun ChatsTab() {
|
||||
LazyColumn(contentPadding = innerPadding) {
|
||||
item {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.public_chat)) },
|
||||
supportingContent = { Text(stringResource(R.string.chat_last_mesaage)) },
|
||||
headlineContent = { Text(stringResource(Res.string.public_chat)) },
|
||||
supportingContent = { Text(stringResource(Res.string.chat_last_mesaage)) },
|
||||
modifier = Modifier.clickable {
|
||||
navController.navigate("chats/publicChat")
|
||||
}
|
||||
|
||||
@@ -22,10 +22,15 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import ru.fromchat.R
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import ru.fromchat.Res
|
||||
import ru.fromchat.chats
|
||||
import ru.fromchat.coming_soon
|
||||
import ru.fromchat.contacts
|
||||
import ru.fromchat.dms
|
||||
import ru.fromchat.utils.exclude
|
||||
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
@Composable
|
||||
fun MainScreen() {
|
||||
var selectedTab by remember { mutableStateOf("chats") }
|
||||
@@ -36,19 +41,19 @@ fun MainScreen() {
|
||||
NavigationBarItem(
|
||||
selected = selectedTab == "chats",
|
||||
onClick = { selectedTab = "chats" },
|
||||
label = { Text(stringResource(R.string.chats)) },
|
||||
label = { Text(stringResource(Res.string.chats)) },
|
||||
icon = { Icon(Icons.AutoMirrored.Filled.Chat, contentDescription = null) }
|
||||
)
|
||||
NavigationBarItem(
|
||||
selected = selectedTab == "contacts",
|
||||
onClick = { selectedTab = "contacts" },
|
||||
label = { Text(stringResource(R.string.contacts)) },
|
||||
label = { Text(stringResource(Res.string.contacts)) },
|
||||
icon = { Icon(Icons.Filled.Contacts, contentDescription = null) }
|
||||
)
|
||||
NavigationBarItem(
|
||||
selected = selectedTab == "dms",
|
||||
onClick = { selectedTab = "dms" },
|
||||
label = { Text(stringResource(R.string.dms)) },
|
||||
label = { Text(stringResource(Res.string.dms)) },
|
||||
icon = { Icon(Icons.Filled.Mail, contentDescription = null) }
|
||||
)
|
||||
}
|
||||
@@ -64,10 +69,10 @@ fun MainScreen() {
|
||||
when (selectedTab) {
|
||||
"chats" -> ChatsTab()
|
||||
"contacts" -> {
|
||||
Text(stringResource(R.string.coming_soon))
|
||||
Text(stringResource(Res.string.coming_soon))
|
||||
}
|
||||
"dms" -> {
|
||||
Text(stringResource(R.string.coming_soon))
|
||||
Text(stringResource(Res.string.coming_soon))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.exclude
|
||||
import androidx.compose.foundation.layout.only
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun WindowInsets.exclude(sides: WindowInsetsSides) = exclude(this.only(sides))
|
||||
Reference in New Issue
Block a user