mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
@@ -1406,7 +1406,11 @@ object ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun logout() {
|
suspend fun logout() {
|
||||||
runCatching { http.get("${ServerConfig.apiBaseUrl}/logout") }
|
runCatching {
|
||||||
|
http.get("${ServerConfig.apiBaseUrl}/logout")
|
||||||
|
}.onFailure { e ->
|
||||||
|
ru.fromchat.Logger.e("ApiClient", "Server logout failed", e)
|
||||||
|
}
|
||||||
runCatching { unregisterFcmTokenFromServer() }
|
runCatching { unregisterFcmTokenFromServer() }
|
||||||
clearLocalSession()
|
clearLocalSession()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ object ProfileCache {
|
|||||||
?: existing?.profilePicture,
|
?: existing?.profilePicture,
|
||||||
bio = existing?.bio,
|
bio = existing?.bio,
|
||||||
online = user.online,
|
online = user.online,
|
||||||
lastSeen = user.last_seen.takeIf { it.isNotBlank() } ?: existing?.lastSeen,
|
lastSeen = user.last_seen?.takeIf { it.isNotBlank() } ?: existing?.lastSeen,
|
||||||
createdAt = user.created_at.takeIf { it.isNotBlank() } ?: existing?.createdAt,
|
createdAt = user.created_at.takeIf { it.isNotBlank() } ?: existing?.createdAt,
|
||||||
verified = user.verified ?: existing?.verified,
|
verified = user.verified ?: existing?.verified,
|
||||||
verificationStatus = user.verificationStatus ?: existing?.verificationStatus,
|
verificationStatus = user.verificationStatus ?: existing?.verificationStatus,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import ru.fromchat.api.schema.user.profile.VerificationStatus
|
|||||||
data class User(
|
data class User(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val created_at: String,
|
val created_at: String,
|
||||||
val last_seen: String,
|
val last_seen: String? = null,
|
||||||
val online: Boolean,
|
val online: Boolean,
|
||||||
val username: String,
|
val username: String,
|
||||||
@SerialName("display_name") val displayName: String? = null,
|
@SerialName("display_name") val displayName: String? = null,
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.safeDrawing
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Info
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
|
import androidx.compose.material.icons.outlined.BugReport
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -68,10 +70,8 @@ fun WelcomeScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(innerPadding),
|
.padding(innerPadding),
|
||||||
) {
|
) {
|
||||||
IconButton(
|
Box(modifier = Modifier.align(Alignment.TopEnd)) {
|
||||||
onClick = { menuExpanded = true },
|
IconButton(onClick = { menuExpanded = true }) {
|
||||||
modifier = Modifier.align(Alignment.TopEnd),
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.MoreVert,
|
imageVector = Icons.Default.MoreVert,
|
||||||
contentDescription = stringResource(Res.string.more),
|
contentDescription = stringResource(Res.string.more),
|
||||||
@@ -83,6 +83,9 @@ fun WelcomeScreen(
|
|||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(Res.string.about)) },
|
text = { Text(stringResource(Res.string.about)) },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(Icons.Default.Info, contentDescription = null)
|
||||||
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
menuExpanded = false
|
menuExpanded = false
|
||||||
navController.navigate(SettingsRoutes.About)
|
navController.navigate(SettingsRoutes.About)
|
||||||
@@ -90,12 +93,16 @@ fun WelcomeScreen(
|
|||||||
)
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(Res.string.logs_title)) },
|
text = { Text(stringResource(Res.string.logs_title)) },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(Icons.Outlined.BugReport, contentDescription = null)
|
||||||
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
menuExpanded = false
|
menuExpanded = false
|
||||||
navController.navigate(SettingsRoutes.Logs)
|
navController.navigate(SettingsRoutes.Logs)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package ru.fromchat.ui.auth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Storage
|
import androidx.compose.material.icons.filled.Storage
|
||||||
import androidx.compose.material3.SnackbarDuration
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -40,7 +39,7 @@ import ru.fromchat.ui.components.ExpressiveStepFlowScaffold
|
|||||||
import ru.fromchat.ui.components.Text
|
import ru.fromchat.ui.components.Text
|
||||||
import ru.fromchat.ui.components.TextCta
|
import ru.fromchat.ui.components.TextCta
|
||||||
import ru.fromchat.ui.components.rememberExpressiveStepFlow
|
import ru.fromchat.ui.components.rememberExpressiveStepFlow
|
||||||
import ru.fromchat.ui.components.showReplacingSnackbar
|
import ru.fromchat.ui.components.showLoggedSnackbar
|
||||||
import ru.fromchat.ui.main.settings.SettingsStepHorizontalPadding
|
import ru.fromchat.ui.main.settings.SettingsStepHorizontalPadding
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@@ -56,13 +55,13 @@ internal sealed interface PasswordStepResult {
|
|||||||
data object AdvanceToRegister : PasswordStepResult
|
data object AdvanceToRegister : PasswordStepResult
|
||||||
data class WrongPassword(val message: String) : PasswordStepResult
|
data class WrongPassword(val message: String) : PasswordStepResult
|
||||||
data class RateLimited(val message: String) : PasswordStepResult
|
data class RateLimited(val message: String) : PasswordStepResult
|
||||||
data class Error(val message: String) : PasswordStepResult
|
data class Error(val message: String, val cause: Throwable? = null) : PasswordStepResult
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed interface RegisterResult {
|
internal sealed interface RegisterResult {
|
||||||
data object Success : RegisterResult
|
data object Success : RegisterResult
|
||||||
data object UsernameTaken : RegisterResult
|
data object UsernameTaken : RegisterResult
|
||||||
data class Error(val message: String) : RegisterResult
|
data class Error(val message: String, val cause: Throwable? = null) : RegisterResult
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val AUTH_SERVER_PROBE_TIMEOUT_MS = 5_000L
|
private const val AUTH_SERVER_PROBE_TIMEOUT_MS = 5_000L
|
||||||
@@ -150,8 +149,8 @@ private suspend fun login(
|
|||||||
|
|
||||||
else -> PasswordStepResult.Error(parseClientError(e, unexpectedError))
|
else -> PasswordStepResult.Error(parseClientError(e, unexpectedError))
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (e: Exception) {
|
||||||
PasswordStepResult.Error(unexpectedError)
|
PasswordStepResult.Error(unexpectedError, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun register(
|
internal suspend fun register(
|
||||||
@@ -186,8 +185,8 @@ internal suspend fun register(
|
|||||||
} else {
|
} else {
|
||||||
RegisterResult.Error(parseClientError(e, unexpectedError))
|
RegisterResult.Error(parseClientError(e, unexpectedError))
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (e: Exception) {
|
||||||
RegisterResult.Error(unexpectedError)
|
RegisterResult.Error(unexpectedError, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun isUsernameTakenError(e: ClientRequestException) =
|
private suspend fun isUsernameTakenError(e: ClientRequestException) =
|
||||||
@@ -232,15 +231,14 @@ fun AuthScreen(
|
|||||||
var displayName by remember { mutableStateOf("") }
|
var displayName by remember { mutableStateOf("") }
|
||||||
var bio by remember { mutableStateOf("") }
|
var bio by remember { mutableStateOf("") }
|
||||||
|
|
||||||
fun snackbar(text: String) {
|
fun snackbar(text: String, cause: Throwable? = null) {
|
||||||
scope.launch {
|
scope.showLoggedSnackbar(
|
||||||
snackbarHostState.showReplacingSnackbar(
|
hostState = snackbarHostState,
|
||||||
message = text,
|
message = text,
|
||||||
withDismissAction = false,
|
logTag = "Auth",
|
||||||
duration = SnackbarDuration.Short,
|
cause = cause,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val resetToUsername: () -> Unit = {
|
val resetToUsername: () -> Unit = {
|
||||||
username = ""
|
username = ""
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ internal fun passwordStepPage(
|
|||||||
onPasswordChange: (String) -> Unit,
|
onPasswordChange: (String) -> Unit,
|
||||||
onLoginSuccess: () -> Unit,
|
onLoginSuccess: () -> Unit,
|
||||||
onRegister: suspend () -> Unit,
|
onRegister: suspend () -> Unit,
|
||||||
onSnackbar: (String) -> Unit,
|
onSnackbar: (String, Throwable?) -> Unit,
|
||||||
): ExpressiveStepPage {
|
): ExpressiveStepPage {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var busy by remember { mutableStateOf(false) }
|
var busy by remember { mutableStateOf(false) }
|
||||||
@@ -112,7 +112,7 @@ internal fun passwordStepPage(
|
|||||||
if (busy) return@ActionButton
|
if (busy) return@ActionButton
|
||||||
|
|
||||||
if (password.length !in 5..50) {
|
if (password.length !in 5..50) {
|
||||||
onSnackbar(pwdLen)
|
onSnackbar(pwdLen, null)
|
||||||
return@ActionButton
|
return@ActionButton
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,15 +138,15 @@ internal fun passwordStepPage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is PasswordStepResult.WrongPassword -> {
|
is PasswordStepResult.WrongPassword -> {
|
||||||
onSnackbar(result.message)
|
onSnackbar(result.message, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
is PasswordStepResult.RateLimited -> {
|
is PasswordStepResult.RateLimited -> {
|
||||||
onSnackbar(result.message)
|
onSnackbar(result.message, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
is PasswordStepResult.Error -> {
|
is PasswordStepResult.Error -> {
|
||||||
onSnackbar(result.message)
|
onSnackbar(result.message, result.cause)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ internal fun usernameStepPage(
|
|||||||
username: String,
|
username: String,
|
||||||
onUsernameChange: (String) -> Unit,
|
onUsernameChange: (String) -> Unit,
|
||||||
onContinue: suspend () -> Unit,
|
onContinue: suspend () -> Unit,
|
||||||
onSnackbar: (String) -> Unit,
|
onSnackbar: (String, Throwable?) -> Unit,
|
||||||
): ExpressiveStepPage {
|
): ExpressiveStepPage {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
@@ -95,11 +95,11 @@ internal fun usernameStepPage(
|
|||||||
if (busy) return@ActionButton
|
if (busy) return@ActionButton
|
||||||
val trimmed = username.trim()
|
val trimmed = username.trim()
|
||||||
if (trimmed.isBlank()) {
|
if (trimmed.isBlank()) {
|
||||||
onSnackbar(fillAll)
|
onSnackbar(fillAll, null)
|
||||||
return@ActionButton
|
return@ActionButton
|
||||||
}
|
}
|
||||||
if (trimmed.length !in 3..20) {
|
if (trimmed.length !in 3..20) {
|
||||||
onSnackbar(usernameLenError)
|
onSnackbar(usernameLenError, null)
|
||||||
return@ActionButton
|
return@ActionButton
|
||||||
}
|
}
|
||||||
onUsernameChange(trimmed)
|
onUsernameChange(trimmed)
|
||||||
@@ -107,7 +107,7 @@ internal fun usernameStepPage(
|
|||||||
busy = true
|
busy = true
|
||||||
try {
|
try {
|
||||||
if (!probeCurrentServer()) {
|
if (!probeCurrentServer()) {
|
||||||
onSnackbar(serverFail)
|
onSnackbar(serverFail, null)
|
||||||
} else {
|
} else {
|
||||||
onContinue()
|
onContinue()
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -50,7 +50,7 @@ internal fun confirmPasswordStepPage(
|
|||||||
onConfirmPasswordChange: (String) -> Unit,
|
onConfirmPasswordChange: (String) -> Unit,
|
||||||
password: String,
|
password: String,
|
||||||
onContinue: suspend () -> Unit,
|
onContinue: suspend () -> Unit,
|
||||||
onSnackbar: (String) -> Unit,
|
onSnackbar: (String, Throwable?) -> Unit,
|
||||||
): ExpressiveStepPage {
|
): ExpressiveStepPage {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
@@ -102,12 +102,12 @@ internal fun confirmPasswordStepPage(
|
|||||||
ActionButton(
|
ActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (confirmPassword.isBlank()) {
|
if (confirmPassword.isBlank()) {
|
||||||
onSnackbar(fillAll)
|
onSnackbar(fillAll, null)
|
||||||
return@ActionButton
|
return@ActionButton
|
||||||
}
|
}
|
||||||
|
|
||||||
if (confirmPassword != password) {
|
if (confirmPassword != password) {
|
||||||
onSnackbar(pwdMatch)
|
onSnackbar(pwdMatch, null)
|
||||||
return@ActionButton
|
return@ActionButton
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ internal fun profileStepPage(
|
|||||||
password: String,
|
password: String,
|
||||||
onRegisterSuccess: () -> Unit,
|
onRegisterSuccess: () -> Unit,
|
||||||
onUsernameTaken: () -> Unit,
|
onUsernameTaken: () -> Unit,
|
||||||
onSnackbar: (String) -> Unit,
|
onSnackbar: (String, Throwable?) -> Unit,
|
||||||
): ExpressiveStepPage {
|
): ExpressiveStepPage {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val fieldColors = expressiveStepFieldColors()
|
val fieldColors = expressiveStepFieldColors()
|
||||||
@@ -128,12 +128,12 @@ internal fun profileStepPage(
|
|||||||
if (busy) return@ActionButton
|
if (busy) return@ActionButton
|
||||||
|
|
||||||
if (displayName.isBlank() || displayName.trim().length > DISPLAY_NAME_MAX) {
|
if (displayName.isBlank() || displayName.trim().length > DISPLAY_NAME_MAX) {
|
||||||
onSnackbar(displayNameError)
|
onSnackbar(displayNameError, null)
|
||||||
return@ActionButton
|
return@ActionButton
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bio.trim().length > BIO_MAX) {
|
if (bio.trim().length > BIO_MAX) {
|
||||||
onSnackbar(unexpected)
|
onSnackbar(unexpected, null)
|
||||||
return@ActionButton
|
return@ActionButton
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,12 +155,12 @@ internal fun profileStepPage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is RegisterResult.UsernameTaken -> {
|
is RegisterResult.UsernameTaken -> {
|
||||||
onSnackbar(usernameTaken)
|
onSnackbar(usernameTaken, null)
|
||||||
onUsernameTaken()
|
onUsernameTaken()
|
||||||
}
|
}
|
||||||
|
|
||||||
is RegisterResult.Error -> {
|
is RegisterResult.Error -> {
|
||||||
onSnackbar(result.message)
|
onSnackbar(result.message, result.cause)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import androidx.compose.material3.SnackbarResult
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import ru.fromchat.Logger
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App-wide snackbar styling: elevated surface container instead of inverse surface.
|
* App-wide snackbar styling: elevated surface container instead of inverse surface.
|
||||||
@@ -38,6 +41,37 @@ fun FromChatSnackbarHost(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs why a snackbar is shown, then displays it via [showReplacingSnackbar].
|
||||||
|
* Use this instead of calling [showReplacingSnackbar] directly when the message is an error.
|
||||||
|
*/
|
||||||
|
fun CoroutineScope.showLoggedSnackbar(
|
||||||
|
hostState: SnackbarHostState,
|
||||||
|
message: String,
|
||||||
|
logTag: String,
|
||||||
|
cause: Throwable? = null,
|
||||||
|
withDismissAction: Boolean = false,
|
||||||
|
duration: SnackbarDuration = SnackbarDuration.Short,
|
||||||
|
) {
|
||||||
|
if (cause != null) {
|
||||||
|
Logger.e(
|
||||||
|
logTag,
|
||||||
|
"Snackbar: $message (${cause::class.simpleName}: ${cause.message})",
|
||||||
|
cause,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Logger.i(logTag, "Snackbar: $message")
|
||||||
|
}
|
||||||
|
|
||||||
|
launch {
|
||||||
|
hostState.showReplacingSnackbar(
|
||||||
|
message = message,
|
||||||
|
withDismissAction = withDismissAction,
|
||||||
|
duration = duration,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Dismisses any visible snackbar, then shows [message] without queueing behind it. */
|
/** Dismisses any visible snackbar, then shows [message] without queueing behind it. */
|
||||||
suspend fun SnackbarHostState.showReplacingSnackbar(
|
suspend fun SnackbarHostState.showReplacingSnackbar(
|
||||||
message: String,
|
message: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user