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() {
|
||||
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() }
|
||||
clearLocalSession()
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ object ProfileCache {
|
||||
?: existing?.profilePicture,
|
||||
bio = existing?.bio,
|
||||
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,
|
||||
verified = user.verified ?: existing?.verified,
|
||||
verificationStatus = user.verificationStatus ?: existing?.verificationStatus,
|
||||
|
||||
@@ -8,7 +8,7 @@ import ru.fromchat.api.schema.user.profile.VerificationStatus
|
||||
data class User(
|
||||
val id: Int,
|
||||
val created_at: String,
|
||||
val last_seen: String,
|
||||
val last_seen: String? = null,
|
||||
val online: Boolean,
|
||||
val username: String,
|
||||
@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.size
|
||||
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.outlined.BugReport
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -68,33 +70,38 @@ fun WelcomeScreen(
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding),
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { menuExpanded = true },
|
||||
modifier = Modifier.align(Alignment.TopEnd),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = stringResource(Res.string.more),
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = menuExpanded,
|
||||
onDismissRequest = { menuExpanded = false },
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(Res.string.about)) },
|
||||
onClick = {
|
||||
menuExpanded = false
|
||||
navController.navigate(SettingsRoutes.About)
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(Res.string.logs_title)) },
|
||||
onClick = {
|
||||
menuExpanded = false
|
||||
navController.navigate(SettingsRoutes.Logs)
|
||||
},
|
||||
)
|
||||
Box(modifier = Modifier.align(Alignment.TopEnd)) {
|
||||
IconButton(onClick = { menuExpanded = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = stringResource(Res.string.more),
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = menuExpanded,
|
||||
onDismissRequest = { menuExpanded = false },
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(Res.string.about)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Info, contentDescription = null)
|
||||
},
|
||||
onClick = {
|
||||
menuExpanded = false
|
||||
navController.navigate(SettingsRoutes.About)
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(Res.string.logs_title)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Outlined.BugReport, contentDescription = null)
|
||||
},
|
||||
onClick = {
|
||||
menuExpanded = false
|
||||
navController.navigate(SettingsRoutes.Logs)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
|
||||
@@ -3,7 +3,6 @@ package ru.fromchat.ui.auth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Storage
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
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.TextCta
|
||||
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 kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@@ -56,13 +55,13 @@ internal sealed interface PasswordStepResult {
|
||||
data object AdvanceToRegister : PasswordStepResult
|
||||
data class WrongPassword(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 {
|
||||
data object Success : 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
|
||||
@@ -150,8 +149,8 @@ private suspend fun login(
|
||||
|
||||
else -> PasswordStepResult.Error(parseClientError(e, unexpectedError))
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
PasswordStepResult.Error(unexpectedError)
|
||||
} catch (e: Exception) {
|
||||
PasswordStepResult.Error(unexpectedError, e)
|
||||
}
|
||||
|
||||
internal suspend fun register(
|
||||
@@ -186,8 +185,8 @@ internal suspend fun register(
|
||||
} else {
|
||||
RegisterResult.Error(parseClientError(e, unexpectedError))
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
RegisterResult.Error(unexpectedError)
|
||||
} catch (e: Exception) {
|
||||
RegisterResult.Error(unexpectedError, e)
|
||||
}
|
||||
|
||||
private suspend fun isUsernameTakenError(e: ClientRequestException) =
|
||||
@@ -232,14 +231,13 @@ fun AuthScreen(
|
||||
var displayName by remember { mutableStateOf("") }
|
||||
var bio by remember { mutableStateOf("") }
|
||||
|
||||
fun snackbar(text: String) {
|
||||
scope.launch {
|
||||
snackbarHostState.showReplacingSnackbar(
|
||||
message = text,
|
||||
withDismissAction = false,
|
||||
duration = SnackbarDuration.Short,
|
||||
)
|
||||
}
|
||||
fun snackbar(text: String, cause: Throwable? = null) {
|
||||
scope.showLoggedSnackbar(
|
||||
hostState = snackbarHostState,
|
||||
message = text,
|
||||
logTag = "Auth",
|
||||
cause = cause,
|
||||
)
|
||||
}
|
||||
|
||||
val resetToUsername: () -> Unit = {
|
||||
|
||||
@@ -54,7 +54,7 @@ internal fun passwordStepPage(
|
||||
onPasswordChange: (String) -> Unit,
|
||||
onLoginSuccess: () -> Unit,
|
||||
onRegister: suspend () -> Unit,
|
||||
onSnackbar: (String) -> Unit,
|
||||
onSnackbar: (String, Throwable?) -> Unit,
|
||||
): ExpressiveStepPage {
|
||||
val scope = rememberCoroutineScope()
|
||||
var busy by remember { mutableStateOf(false) }
|
||||
@@ -112,7 +112,7 @@ internal fun passwordStepPage(
|
||||
if (busy) return@ActionButton
|
||||
|
||||
if (password.length !in 5..50) {
|
||||
onSnackbar(pwdLen)
|
||||
onSnackbar(pwdLen, null)
|
||||
return@ActionButton
|
||||
}
|
||||
|
||||
@@ -138,15 +138,15 @@ internal fun passwordStepPage(
|
||||
}
|
||||
|
||||
is PasswordStepResult.WrongPassword -> {
|
||||
onSnackbar(result.message)
|
||||
onSnackbar(result.message, null)
|
||||
}
|
||||
|
||||
is PasswordStepResult.RateLimited -> {
|
||||
onSnackbar(result.message)
|
||||
onSnackbar(result.message, null)
|
||||
}
|
||||
|
||||
is PasswordStepResult.Error -> {
|
||||
onSnackbar(result.message)
|
||||
onSnackbar(result.message, result.cause)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -45,7 +45,7 @@ internal fun usernameStepPage(
|
||||
username: String,
|
||||
onUsernameChange: (String) -> Unit,
|
||||
onContinue: suspend () -> Unit,
|
||||
onSnackbar: (String) -> Unit,
|
||||
onSnackbar: (String, Throwable?) -> Unit,
|
||||
): ExpressiveStepPage {
|
||||
val scope = rememberCoroutineScope()
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
@@ -95,11 +95,11 @@ internal fun usernameStepPage(
|
||||
if (busy) return@ActionButton
|
||||
val trimmed = username.trim()
|
||||
if (trimmed.isBlank()) {
|
||||
onSnackbar(fillAll)
|
||||
onSnackbar(fillAll, null)
|
||||
return@ActionButton
|
||||
}
|
||||
if (trimmed.length !in 3..20) {
|
||||
onSnackbar(usernameLenError)
|
||||
onSnackbar(usernameLenError, null)
|
||||
return@ActionButton
|
||||
}
|
||||
onUsernameChange(trimmed)
|
||||
@@ -107,7 +107,7 @@ internal fun usernameStepPage(
|
||||
busy = true
|
||||
try {
|
||||
if (!probeCurrentServer()) {
|
||||
onSnackbar(serverFail)
|
||||
onSnackbar(serverFail, null)
|
||||
} else {
|
||||
onContinue()
|
||||
}
|
||||
|
||||
+3
-3
@@ -50,7 +50,7 @@ internal fun confirmPasswordStepPage(
|
||||
onConfirmPasswordChange: (String) -> Unit,
|
||||
password: String,
|
||||
onContinue: suspend () -> Unit,
|
||||
onSnackbar: (String) -> Unit,
|
||||
onSnackbar: (String, Throwable?) -> Unit,
|
||||
): ExpressiveStepPage {
|
||||
val scope = rememberCoroutineScope()
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
@@ -102,12 +102,12 @@ internal fun confirmPasswordStepPage(
|
||||
ActionButton(
|
||||
onClick = {
|
||||
if (confirmPassword.isBlank()) {
|
||||
onSnackbar(fillAll)
|
||||
onSnackbar(fillAll, null)
|
||||
return@ActionButton
|
||||
}
|
||||
|
||||
if (confirmPassword != password) {
|
||||
onSnackbar(pwdMatch)
|
||||
onSnackbar(pwdMatch, null)
|
||||
return@ActionButton
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ internal fun profileStepPage(
|
||||
password: String,
|
||||
onRegisterSuccess: () -> Unit,
|
||||
onUsernameTaken: () -> Unit,
|
||||
onSnackbar: (String) -> Unit,
|
||||
onSnackbar: (String, Throwable?) -> Unit,
|
||||
): ExpressiveStepPage {
|
||||
val scope = rememberCoroutineScope()
|
||||
val fieldColors = expressiveStepFieldColors()
|
||||
@@ -128,12 +128,12 @@ internal fun profileStepPage(
|
||||
if (busy) return@ActionButton
|
||||
|
||||
if (displayName.isBlank() || displayName.trim().length > DISPLAY_NAME_MAX) {
|
||||
onSnackbar(displayNameError)
|
||||
onSnackbar(displayNameError, null)
|
||||
return@ActionButton
|
||||
}
|
||||
|
||||
if (bio.trim().length > BIO_MAX) {
|
||||
onSnackbar(unexpected)
|
||||
onSnackbar(unexpected, null)
|
||||
return@ActionButton
|
||||
}
|
||||
|
||||
@@ -155,12 +155,12 @@ internal fun profileStepPage(
|
||||
}
|
||||
|
||||
is RegisterResult.UsernameTaken -> {
|
||||
onSnackbar(usernameTaken)
|
||||
onSnackbar(usernameTaken, null)
|
||||
onUsernameTaken()
|
||||
}
|
||||
|
||||
is RegisterResult.Error -> {
|
||||
onSnackbar(result.message)
|
||||
onSnackbar(result.message, result.cause)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -10,6 +10,9 @@ import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
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.
|
||||
@@ -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. */
|
||||
suspend fun SnackbarHostState.showReplacingSnackbar(
|
||||
message: String,
|
||||
|
||||
Reference in New Issue
Block a user