feat: add profile language preferences and refine toast, dropdown and history UI

This commit is contained in:
Mateusz Gruszczyński
2026-09-04 23:48:09 +02:00
parent f036240d5d
commit bf13587a71
42 changed files with 3971 additions and 357 deletions
+28
View File
@@ -51,6 +51,7 @@ pub struct User {
pub confirmed_at: Option<String>,
pub is_active: i64,
pub theme: String,
pub language: String,
}
#[derive(Deserialize)]
@@ -99,6 +100,8 @@ pub struct ProfileUpdateRequest {
editor_color: Option<String>,
#[serde(default)]
theme: Option<String>,
#[serde(default)]
language: Option<String>,
}
#[derive(Deserialize)]
pub struct DeleteAccountRequest {
@@ -252,6 +255,7 @@ impl<'r> sqlx::FromRow<'r, AnyRow> for User {
confirmed_at: crate::row_decode::optional_text(row, "confirmed_at")?,
is_active: row.try_get("is_active")?,
theme: crate::row_decode::text(row, "theme")?,
language: crate::row_decode::text(row, "language")?,
})
}
}
@@ -367,6 +371,7 @@ pub struct SessionResponse {
suggested_nickname: Option<String>,
editor_color: Option<String>,
theme: String,
language: String,
}
#[derive(Serialize)]
pub struct IdentityResponse {
@@ -380,6 +385,7 @@ pub struct RegisterResponse {
expires_at: Option<String>,
confirmation_required: bool,
theme: String,
language: String,
message: String,
}
@@ -531,6 +537,7 @@ pub async fn register(
expires_at: None,
confirmation_required: true,
theme: user.theme,
language: user.language,
message:
"Account created. Check your e-mail and confirm the account before logging in."
.into(),
@@ -550,6 +557,7 @@ pub async fn register(
expires_at: Some(session.expires_at),
confirmation_required: false,
theme: session.theme,
language: session.language,
message: "Account created.".into(),
}),
)
@@ -845,6 +853,7 @@ pub async fn me(
suggested_nickname,
editor_color,
theme: user.theme,
language: user.language,
},
state.user_session_ttl_days,
))
@@ -972,11 +981,24 @@ pub async fn update_profile(
.map_err(AuthError::database)?;
}
let mut language = user.language.clone();
if let Some(value) = req.language.as_deref() {
language = validate_language(value)?.to_owned();
sqlx::query(queries::get(state.db.kind(), queries::AUTH_UPDATE_LANGUAGE))
.bind(&language)
.bind(Utc::now().to_rfc3339())
.bind(user.id)
.execute(state.db.pool())
.await
.map_err(AuthError::database)?;
}
Ok(Json(serde_json::json!({
"ok": true,
"nickname": nickname,
"editor_color": req.editor_color.as_deref(),
"theme": theme,
"language": language,
"email_pending": email_pending,
"message": if email_pending {
"Profile updated. Confirm the new e-mail address using the link sent to it."
@@ -2560,6 +2582,7 @@ async fn create_session(state: &SharedState, user: &User) -> Result<SessionRespo
suggested_nickname,
editor_color,
theme: user.theme.clone(),
language: user.language.clone(),
})
}
async fn find_user_by_nickname(
@@ -2620,6 +2643,11 @@ fn validate_theme(value: &str) -> Result<&str, AuthError> {
}
}
fn validate_language(value: &str) -> Result<&'static str, AuthError> {
crate::i18n::canonical_code(value)
.ok_or_else(|| AuthError::bad_request("Unknown interface language."))
}
fn validate_editor_color(value: &str) -> Result<String, AuthError> {
let value = value.trim();
if value.len() == 7