diff --git a/apps/chattyness-app/src/app.rs b/apps/chattyness-app/src/app.rs index 75ba5dc..bf7a632 100644 --- a/apps/chattyness-app/src/app.rs +++ b/apps/chattyness-app/src/app.rs @@ -4,16 +4,14 @@ //! with the admin interface lazy-loaded to reduce initial WASM bundle size. use leptos::prelude::*; -use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title}; +use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context}; use leptos_router::{ - components::{Route, Router, Routes}, ParamSegment, StaticSegment, + components::{Route, Router, Routes}, }; // Re-export user pages for inline route definitions -use chattyness_user_ui::pages::{ - HomePage, LoginPage, PasswordResetPage, RealmPage, SignupPage, -}; +use chattyness_user_ui::pages::{HomePage, LoginPage, PasswordResetPage, RealmPage, SignupPage}; // Lazy-load admin pages to split WASM bundle // Each lazy function includes the admin CSS stylesheet for on-demand loading @@ -34,7 +32,8 @@ fn lazy_login() -> AnyView { - }.into_any() + } + .into_any() } #[lazy] diff --git a/apps/chattyness-app/src/lib.rs b/apps/chattyness-app/src/lib.rs index 96ab28a..29fbb86 100644 --- a/apps/chattyness-app/src/lib.rs +++ b/apps/chattyness-app/src/lib.rs @@ -6,7 +6,7 @@ mod app; -pub use app::{combined_shell, CombinedApp}; +pub use app::{CombinedApp, combined_shell}; #[cfg(feature = "ssr")] pub use app::CombinedAppState; diff --git a/apps/chattyness-app/src/main.rs b/apps/chattyness-app/src/main.rs index 79019da..fcec49f 100644 --- a/apps/chattyness-app/src/main.rs +++ b/apps/chattyness-app/src/main.rs @@ -11,7 +11,7 @@ mod server { use axum::Router; use clap::Parser; use leptos::prelude::*; - use leptos_axum::{generate_route_list, LeptosRoutes}; + use leptos_axum::{LeptosRoutes, generate_route_list}; use sqlx::postgres::PgPoolOptions; use std::net::SocketAddr; use std::path::{Path, PathBuf}; @@ -20,7 +20,7 @@ mod server { use tower_http::services::ServeDir; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; - use chattyness_app::{combined_shell, CombinedApp, CombinedAppState}; + use chattyness_app::{CombinedApp, CombinedAppState, combined_shell}; use chattyness_shared::AppConfig; use chattyness_user_ui::api::WebSocketState; @@ -57,8 +57,9 @@ mod server { // Initialize logging tracing_subscriber::registry() .with( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "chattyness_app=debug,chattyness_user_ui=debug,tower_http=debug".into()), + tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { + "chattyness_app=debug,chattyness_user_ui=debug,tower_http=debug".into() + }), ) .with(tracing_subscriber::fmt::layer()) .init(); @@ -100,9 +101,8 @@ mod server { let cleanup_pool = pool.clone(); let cleanup_config = config.cleanup.clone(); tokio::spawn(async move { - let mut interval = tokio::time::interval(Duration::from_secs( - cleanup_config.reap_interval_secs, - )); + let mut interval = + tokio::time::interval(Duration::from_secs(cleanup_config.reap_interval_secs)); loop { interval.tick().await; let threshold = cleanup_config.stale_threshold_secs as f64; @@ -138,9 +138,11 @@ mod server { let addr = SocketAddr::new(args.host.parse()?, args.port); // Create session layer (shared between user and admin interfaces) - let session_layer = - chattyness_user_ui::auth::session::create_session_layer(pool.clone(), args.secure_cookies) - .await; + let session_layer = chattyness_user_ui::auth::session::create_session_layer( + pool.clone(), + args.secure_cookies, + ) + .await; // Create combined app state let app_state = CombinedAppState { @@ -183,10 +185,9 @@ mod server { }; // Build nested API routers with their own state - let user_api_router = chattyness_user_ui::api::api_router() - .with_state(user_api_state); - let admin_api_router = chattyness_admin_ui::api::admin_api_router() - .with_state(admin_api_state); + let user_api_router = chattyness_user_ui::api::api_router().with_state(user_api_state); + let admin_api_router = + chattyness_admin_ui::api::admin_api_router().with_state(admin_api_state); // Create RLS layer for row-level security let rls_layer = chattyness_user_ui::auth::RlsLayer::new(pool.clone()); @@ -216,16 +217,30 @@ mod server { .with_state(app_state) // Serve pkg files at /pkg (wasm_split hardcodes /pkg/ imports) // Fallback to split_pkg_dir for --split mode output - .nest_service("/pkg", ServeDir::new(&pkg_dir).fallback(ServeDir::new(&split_pkg_dir))) + .nest_service( + "/pkg", + ServeDir::new(&pkg_dir).fallback(ServeDir::new(&split_pkg_dir)), + ) // Uploaded assets (realm backgrounds, etc.) - must come before /static .nest_service("/static/realm", ServeDir::new(assets_dir.join("realm"))) // Server-level assets (avatar props, etc.) .nest_service("/static/server", ServeDir::new(assets_dir.join("server"))) // Also serve at /static for backwards compatibility - .nest_service("/static", ServeDir::new(&pkg_dir).fallback(ServeDir::new(&split_pkg_dir))) - .nest_service("/favicon.ico", tower_http::services::ServeFile::new(public_dir.join("favicon.ico"))) + .nest_service( + "/static", + ServeDir::new(&pkg_dir).fallback(ServeDir::new(&split_pkg_dir)), + ) + .nest_service( + "/favicon.ico", + tower_http::services::ServeFile::new(public_dir.join("favicon.ico")), + ) + // Serve icons from public/icons + .nest_service("/icons", ServeDir::new(public_dir.join("icons"))) // Serve admin CSS at standardized path (symlinked from owner build) - .nest_service("/static/css/admin.css", tower_http::services::ServeFile::new(public_dir.join("admin.css"))) + .nest_service( + "/static/css/admin.css", + tower_http::services::ServeFile::new(public_dir.join("admin.css")), + ) // Apply middleware layers (order: session outer, rls inner) .layer(rls_layer) .layer(session_layer); diff --git a/apps/chattyness-owner/src/main.rs b/apps/chattyness-owner/src/main.rs index ecfa53b..65dcdf1 100644 --- a/apps/chattyness-owner/src/main.rs +++ b/apps/chattyness-owner/src/main.rs @@ -6,17 +6,17 @@ #[cfg(feature = "ssr")] mod server { - use axum::{response::Redirect, routing::get, Router}; + use axum::{Router, response::Redirect, routing::get}; use clap::Parser; use leptos::prelude::*; - use leptos_axum::{generate_route_list, LeptosRoutes}; + use leptos_axum::{LeptosRoutes, generate_route_list}; use sqlx::postgres::PgPoolOptions; use std::net::SocketAddr; use std::path::Path; use tower_http::services::ServeDir; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; - use chattyness_admin_ui::{admin_shell, AdminApp, AdminAppState}; + use chattyness_admin_ui::{AdminApp, AdminAppState, admin_shell}; /// CLI arguments. #[derive(Parser)] @@ -77,9 +77,11 @@ mod server { let addr = SocketAddr::new(args.host.parse()?, args.port); // Create session layer - let session_layer = - chattyness_admin_ui::auth::create_admin_session_layer(pool.clone(), args.secure_cookies) - .await; + let session_layer = chattyness_admin_ui::auth::create_admin_session_layer( + pool.clone(), + args.secure_cookies, + ) + .await; // Create app state let app_state = AdminAppState { @@ -111,15 +113,24 @@ mod server { // Redirect root to admin .route("/", get(|| async { Redirect::permanent("/admin") })) // Nest API routes under /api/admin (matches frontend expectations when UI is at /admin) - .nest("/api/admin", chattyness_admin_ui::api::admin_api_router().with_state(app_state.clone())) + .nest( + "/api/admin", + chattyness_admin_ui::api::admin_api_router().with_state(app_state.clone()), + ) // Uploaded assets (realm backgrounds, props, etc.) - must come before /static .nest_service("/assets/server", ServeDir::new(assets_dir.join("server"))) .nest_service("/static/realm", ServeDir::new(assets_dir.join("realm"))) // Static files (build output: JS, CSS, WASM) .nest_service("/static", ServeDir::new(&static_dir)) - .nest_service("/favicon.ico", tower_http::services::ServeFile::new(&favicon_path)) + .nest_service( + "/favicon.ico", + tower_http::services::ServeFile::new(&favicon_path), + ) // Serve admin CSS at standardized path - .nest_service("/static/css/admin.css", tower_http::services::ServeFile::new(&admin_css_path)) + .nest_service( + "/static/css/admin.css", + tower_http::services::ServeFile::new(&admin_css_path), + ) // Leptos routes .leptos_routes(&app_state, routes, { let leptos_options = leptos_options.clone(); diff --git a/crates/chattyness-admin-ui/src/api/auth.rs b/crates/chattyness-admin-ui/src/api/auth.rs index 497c1b6..05b834a 100644 --- a/crates/chattyness-admin-ui/src/api/auth.rs +++ b/crates/chattyness-admin-ui/src/api/auth.rs @@ -1,6 +1,6 @@ //! Admin authentication API handlers. -use axum::{extract::State, http::StatusCode, Json}; +use axum::{Json, extract::State, http::StatusCode}; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use tower_sessions::Session; @@ -221,21 +221,16 @@ pub async fn get_auth_context( session: Session, ) -> Result, (StatusCode, Json)> { // Try to get staff_id from session (server staff) - let staff_id: Option = session - .get(ADMIN_SESSION_STAFF_ID_KEY) - .await - .ok() - .flatten(); + let staff_id: Option = session.get(ADMIN_SESSION_STAFF_ID_KEY).await.ok().flatten(); if let Some(staff_id) = staff_id { // Check if this is actually a staff member - let is_staff: Option = sqlx::query_scalar( - "SELECT EXISTS(SELECT 1 FROM server.staff WHERE user_id = $1)", - ) - .bind(staff_id) - .fetch_one(&pool) - .await - .ok(); + let is_staff: Option = + sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM server.staff WHERE user_id = $1)") + .bind(staff_id) + .fetch_one(&pool) + .await + .ok(); if is_staff == Some(true) { return Ok(Json(AuthContextResponse { diff --git a/crates/chattyness-admin-ui/src/api/config.rs b/crates/chattyness-admin-ui/src/api/config.rs index 2546faa..7b69dbc 100644 --- a/crates/chattyness-admin-ui/src/api/config.rs +++ b/crates/chattyness-admin-ui/src/api/config.rs @@ -1,6 +1,6 @@ //! Server config API handlers. -use axum::{extract::State, Json}; +use axum::{Json, extract::State}; use chattyness_db::{ models::{ServerConfig, UpdateServerConfigRequest}, queries::owner as queries, @@ -9,9 +9,7 @@ use chattyness_error::AppError; use sqlx::PgPool; /// Get server config. -pub async fn get_config( - State(pool): State, -) -> Result, AppError> { +pub async fn get_config(State(pool): State) -> Result, AppError> { let config = queries::get_server_config(&pool).await?; Ok(Json(config)) } diff --git a/crates/chattyness-admin-ui/src/api/dashboard.rs b/crates/chattyness-admin-ui/src/api/dashboard.rs index 2dd1ac1..150add3 100644 --- a/crates/chattyness-admin-ui/src/api/dashboard.rs +++ b/crates/chattyness-admin-ui/src/api/dashboard.rs @@ -1,6 +1,6 @@ //! Dashboard API handlers. -use axum::{extract::State, Json}; +use axum::{Json, extract::State}; use chattyness_error::AppError; use serde::Serialize; use sqlx::PgPool; @@ -16,9 +16,7 @@ pub struct DashboardStats { } /// Get dashboard stats. -pub async fn get_stats( - State(pool): State, -) -> Result, AppError> { +pub async fn get_stats(State(pool): State) -> Result, AppError> { // Total users let total_users: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM auth.users") .fetch_one(&pool) diff --git a/crates/chattyness-admin-ui/src/api/props.rs b/crates/chattyness-admin-ui/src/api/props.rs index b018666..bad536a 100644 --- a/crates/chattyness-admin-ui/src/api/props.rs +++ b/crates/chattyness-admin-ui/src/api/props.rs @@ -1,14 +1,14 @@ //! Props management API handlers for admin UI. -use axum::extract::{Query, State}; use axum::Json; +use axum::extract::{Query, State}; use axum_extra::extract::Multipart; -use serde::Deserialize; use chattyness_db::{ models::{CreateServerPropRequest, ServerProp, ServerPropSummary}, queries::props, }; use chattyness_error::AppError; +use serde::Deserialize; use serde::Serialize; use sha2::{Digest, Sha256}; use sqlx::PgPool; @@ -64,9 +64,7 @@ fn validate_file_extension(filename: &str) -> Result<&'static str, AppError> { match ext.as_str() { "svg" => Ok("svg"), "png" => Ok("png"), - _ => Err(AppError::Validation( - "File must be SVG or PNG".to_string(), - )), + _ => Err(AppError::Validation("File must be SVG or PNG".to_string())), } } @@ -101,7 +99,9 @@ async fn store_prop_file(bytes: &[u8], extension: &str) -> Result) -> Result>, AppError> { +pub async fn list_props( + State(pool): State, +) -> Result>, AppError> { let prop_list = props::list_server_props(&pool).await?; Ok(Json(prop_list)) } @@ -137,9 +137,10 @@ pub async fn create_prop( .await .map_err(|e| AppError::Validation(format!("Failed to read metadata: {}", e)))?; - metadata = Some(serde_json::from_str(&text).map_err(|e| { - AppError::Validation(format!("Invalid metadata JSON: {}", e)) - })?); + metadata = + Some(serde_json::from_str(&text).map_err(|e| { + AppError::Validation(format!("Invalid metadata JSON: {}", e)) + })?); } "file" => { let filename = field diff --git a/crates/chattyness-admin-ui/src/api/realms.rs b/crates/chattyness-admin-ui/src/api/realms.rs index 72ad45d..d2f31ff 100644 --- a/crates/chattyness-admin-ui/src/api/realms.rs +++ b/crates/chattyness-admin-ui/src/api/realms.rs @@ -1,8 +1,8 @@ //! Realm management API handlers. use axum::{ - extract::{Path, Query, State}, Json, + extract::{Path, Query, State}, }; use chattyness_db::{ models::{OwnerCreateRealmRequest, RealmDetail, RealmListItem, UpdateRealmRequest}, diff --git a/crates/chattyness-admin-ui/src/api/routes.rs b/crates/chattyness-admin-ui/src/api/routes.rs index b8e7055..fda79d0 100644 --- a/crates/chattyness-admin-ui/src/api/routes.rs +++ b/crates/chattyness-admin-ui/src/api/routes.rs @@ -1,8 +1,8 @@ //! Admin API routes. use axum::{ - routing::{delete, get, post, put}, Router, + routing::{delete, get, post, put}, }; use super::{auth, config, dashboard, props, realms, scenes, spots, staff, users}; @@ -56,10 +56,7 @@ pub fn admin_api_router() -> Router { "/realms/{slug}", get(realms::get_realm).put(realms::update_realm), ) - .route( - "/realms/{slug}/transfer", - post(realms::transfer_ownership), - ) + .route("/realms/{slug}/transfer", post(realms::transfer_ownership)) // API - Scenes .route( "/realms/{slug}/scenes", diff --git a/crates/chattyness-admin-ui/src/api/scenes.rs b/crates/chattyness-admin-ui/src/api/scenes.rs index 62d11e7..cebc006 100644 --- a/crates/chattyness-admin-ui/src/api/scenes.rs +++ b/crates/chattyness-admin-ui/src/api/scenes.rs @@ -1,8 +1,8 @@ //! Scene management API handlers for admin UI. use axum::{ - extract::{Path, State}, Json, + extract::{Path, State}, }; use chattyness_db::{ models::{CreateSceneRequest, Scene, SceneSummary, UpdateSceneRequest}, @@ -136,10 +136,7 @@ async fn download_and_store_image( .map_err(|e| AppError::Internal(format!("Failed to write image file: {}", e)))?; // Return the URL path (relative to server root) - let local_path = format!( - "/static/realm/{}/scene/{}/{}", - realm_id, scene_id, filename - ); + let local_path = format!("/static/realm/{}/scene/{}/{}", realm_id, scene_id, filename); Ok(ImageDownloadResult { local_path, @@ -237,13 +234,9 @@ pub async fn create_scene( // Handle background image URL - download and store locally if let Some(ref url) = req.background_image_url { if !url.is_empty() { - let result = download_and_store_image( - url, - realm.id, - scene_id, - req.infer_dimensions_from_image, - ) - .await?; + let result = + download_and_store_image(url, realm.id, scene_id, req.infer_dimensions_from_image) + .await?; req.background_image_path = Some(result.local_path); diff --git a/crates/chattyness-admin-ui/src/api/spots.rs b/crates/chattyness-admin-ui/src/api/spots.rs index db83212..527f86b 100644 --- a/crates/chattyness-admin-ui/src/api/spots.rs +++ b/crates/chattyness-admin-ui/src/api/spots.rs @@ -1,8 +1,8 @@ //! Spot management API handlers for admin UI. use axum::{ - extract::{Path, State}, Json, + extract::{Path, State}, }; use chattyness_db::{ models::{CreateSpotRequest, Spot, SpotSummary, UpdateSpotRequest}, @@ -73,7 +73,8 @@ pub async fn update_spot( .ok_or_else(|| AppError::NotFound("Spot not found".to_string()))?; if Some(new_slug.clone()) != existing.slug { - let available = spots::is_spot_slug_available(&pool, existing.scene_id, new_slug).await?; + let available = + spots::is_spot_slug_available(&pool, existing.scene_id, new_slug).await?; if !available { return Err(AppError::Conflict(format!( "Spot slug '{}' is already taken in this scene", diff --git a/crates/chattyness-admin-ui/src/api/staff.rs b/crates/chattyness-admin-ui/src/api/staff.rs index daf9f9b..bcc0477 100644 --- a/crates/chattyness-admin-ui/src/api/staff.rs +++ b/crates/chattyness-admin-ui/src/api/staff.rs @@ -1,8 +1,8 @@ //! Staff management API handlers. use axum::{ - extract::{Path, State}, Json, + extract::{Path, State}, }; use chattyness_db::{ models::{CreateStaffRequest, StaffMember}, @@ -21,9 +21,7 @@ pub struct CreateStaffResponse { } /// List all staff members. -pub async fn list_staff( - State(pool): State, -) -> Result>, AppError> { +pub async fn list_staff(State(pool): State) -> Result>, AppError> { let staff = queries::get_all_staff(&pool).await?; Ok(Json(staff)) } @@ -44,11 +42,7 @@ pub async fn create_staff( })) } else if let Some(ref new_user) = req.new_user { // Create new user and promote to staff - let (user_id, temporary_password) = queries::create_user( - &pool, - new_user, - ) - .await?; + let (user_id, temporary_password) = queries::create_user(&pool, new_user).await?; let staff = queries::create_staff(&pool, user_id, req.role, None).await?; Ok(Json(CreateStaffResponse { staff, diff --git a/crates/chattyness-admin-ui/src/api/users.rs b/crates/chattyness-admin-ui/src/api/users.rs index 837645e..4f61fba 100644 --- a/crates/chattyness-admin-ui/src/api/users.rs +++ b/crates/chattyness-admin-ui/src/api/users.rs @@ -1,8 +1,8 @@ //! User management API handlers. use axum::{ - extract::{Path, Query, State}, Json, + extract::{Path, Query, State}, }; use chattyness_db::{ models::{ @@ -153,9 +153,7 @@ pub async fn remove_from_realm( } /// List all realms (for dropdown). -pub async fn list_realms( - State(pool): State, -) -> Result>, AppError> { +pub async fn list_realms(State(pool): State) -> Result>, AppError> { let realms = queries::list_all_realms(&pool).await?; Ok(Json(realms)) } diff --git a/crates/chattyness-admin-ui/src/app.rs b/crates/chattyness-admin-ui/src/app.rs index cac693b..602288b 100644 --- a/crates/chattyness-admin-ui/src/app.rs +++ b/crates/chattyness-admin-ui/src/app.rs @@ -1,7 +1,7 @@ //! Admin Leptos application root and router. use leptos::prelude::*; -use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title}; +use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context}; use leptos_router::components::Router; use crate::routes::AdminRoutes; diff --git a/crates/chattyness-admin-ui/src/auth.rs b/crates/chattyness-admin-ui/src/auth.rs index adefe45..fc328a2 100644 --- a/crates/chattyness-admin-ui/src/auth.rs +++ b/crates/chattyness-admin-ui/src/auth.rs @@ -12,7 +12,7 @@ use axum::{ #[cfg(feature = "ssr")] use sqlx::PgPool; #[cfg(feature = "ssr")] -use tower_sessions::{cookie::time::Duration, cookie::SameSite, Expiry, SessionManagerLayer}; +use tower_sessions::{Expiry, SessionManagerLayer, cookie::SameSite, cookie::time::Duration}; #[cfg(feature = "ssr")] use tower_sessions_sqlx_store::PostgresStore; #[cfg(feature = "ssr")] diff --git a/crates/chattyness-admin-ui/src/components.rs b/crates/chattyness-admin-ui/src/components.rs index 0551aed..1931959 100644 --- a/crates/chattyness-admin-ui/src/components.rs +++ b/crates/chattyness-admin-ui/src/components.rs @@ -110,8 +110,7 @@ pub fn use_auth_context() -> LocalResource> { #[component] pub fn AuthenticatedLayout( current_page: &'static str, - #[prop(default = "/admin")] - base_path: &'static str, + #[prop(default = "/admin")] base_path: &'static str, children: ChildrenFn, ) -> impl IntoView { let auth_context = use_auth_context(); @@ -165,10 +164,8 @@ pub fn AuthenticatedLayout( fn Sidebar( current_page: &'static str, base_path: &'static str, - #[prop(default = false)] - is_server_staff: bool, - #[prop(default = vec![])] - managed_realms: Vec<(String, String)>, + #[prop(default = false)] is_server_staff: bool, + #[prop(default = vec![])] managed_realms: Vec<(String, String)>, ) -> impl IntoView { // Build hrefs with base path let dashboard_href = base_path.to_string(); @@ -319,13 +316,22 @@ fn NavItem( label: &'static str, #[prop(default = false)] active: bool, /// Whether this is a sub-item (indented) - #[prop(default = false)] sub: bool, + #[prop(default = false)] + sub: bool, ) -> impl IntoView { let link_class = match (active, sub) { - (true, false) => "block w-full px-6 py-2 bg-violet-600 text-white transition-all duration-150", - (false, false) => "block w-full px-6 py-2 text-gray-400 hover:bg-slate-700 hover:text-white transition-all duration-150", - (true, true) => "block w-full pl-10 pr-6 py-2 text-sm bg-violet-600 text-white transition-all duration-150", - (false, true) => "block w-full pl-10 pr-6 py-2 text-sm text-gray-400 hover:bg-slate-700 hover:text-white transition-all duration-150", + (true, false) => { + "block w-full px-6 py-2 bg-violet-600 text-white transition-all duration-150" + } + (false, false) => { + "block w-full px-6 py-2 text-gray-400 hover:bg-slate-700 hover:text-white transition-all duration-150" + } + (true, true) => { + "block w-full pl-10 pr-6 py-2 text-sm bg-violet-600 text-white transition-all duration-150" + } + (false, true) => { + "block w-full pl-10 pr-6 py-2 text-sm text-gray-400 hover:bg-slate-700 hover:text-white transition-all duration-150" + } }; view! { diff --git a/crates/chattyness-admin-ui/src/hooks.rs b/crates/chattyness-admin-ui/src/hooks.rs index ef1f864..4493d0c 100644 --- a/crates/chattyness-admin-ui/src/hooks.rs +++ b/crates/chattyness-admin-ui/src/hooks.rs @@ -154,11 +154,7 @@ where }; let response = if let Some(body) = body { - request - .json(body) - .map_err(|e| e.to_string())? - .send() - .await + request.json(body).map_err(|e| e.to_string())?.send().await } else { request.send().await } @@ -200,11 +196,7 @@ pub async fn api_request_simple( }; let response = if let Some(body) = body { - request - .json(body) - .map_err(|e| e.to_string())? - .send() - .await + request.json(body).map_err(|e| e.to_string())?.send().await } else { request.send().await } diff --git a/crates/chattyness-admin-ui/src/lib.rs b/crates/chattyness-admin-ui/src/lib.rs index 03ddd05..4bac58b 100644 --- a/crates/chattyness-admin-ui/src/lib.rs +++ b/crates/chattyness-admin-ui/src/lib.rs @@ -31,7 +31,7 @@ pub mod pages; pub mod routes; pub mod utils; -pub use app::{admin_shell, AdminApp}; +pub use app::{AdminApp, admin_shell}; pub use routes::AdminRoutes; // Re-export commonly used items for convenience @@ -40,7 +40,7 @@ pub use components::{ MessageAlert, MessageAlertRw, NsfwBadge, PageHeader, Pagination, PrivacyBadge, RoleBadge, SearchForm, StatusBadge, SubmitButton, TempPasswordDisplay, }; -pub use hooks::{use_fetch, use_fetch_if, use_message, use_pagination, PaginationState}; +pub use hooks::{PaginationState, use_fetch, use_fetch_if, use_message, use_pagination}; pub use models::*; pub use utils::{build_bounds_wkt, build_paginated_url, get_api_base, parse_bounds_wkt}; diff --git a/crates/chattyness-admin-ui/src/pages/realm_detail.rs b/crates/chattyness-admin-ui/src/pages/realm_detail.rs index 31ce4f5..34576a2 100644 --- a/crates/chattyness-admin-ui/src/pages/realm_detail.rs +++ b/crates/chattyness-admin-ui/src/pages/realm_detail.rs @@ -1,9 +1,9 @@ //! Realm detail/edit page component. use leptos::prelude::*; -use leptos_router::hooks::use_params_map; #[cfg(feature = "hydrate")] use leptos::task::spawn_local; +use leptos_router::hooks::use_params_map; use crate::components::{ Card, DetailGrid, DetailItem, MessageAlert, NsfwBadge, PageHeader, PrivacyBadge, @@ -73,8 +73,12 @@ fn RealmDetailView( let (max_users, set_max_users) = signal(realm.max_users); let (is_nsfw, set_is_nsfw) = signal(realm.is_nsfw); let (allow_guest_access, set_allow_guest_access) = signal(realm.allow_guest_access); - let (theme_color, set_theme_color) = - signal(realm.theme_color.clone().unwrap_or_else(|| "#7c3aed".to_string())); + let (theme_color, set_theme_color) = signal( + realm + .theme_color + .clone() + .unwrap_or_else(|| "#7c3aed".to_string()), + ); let on_submit = move |ev: leptos::ev::SubmitEvent| { ev.prevent_default(); diff --git a/crates/chattyness-admin-ui/src/pages/realm_new.rs b/crates/chattyness-admin-ui/src/pages/realm_new.rs index effd569..f520428 100644 --- a/crates/chattyness-admin-ui/src/pages/realm_new.rs +++ b/crates/chattyness-admin-ui/src/pages/realm_new.rs @@ -80,8 +80,14 @@ pub fn RealmNewPage() -> impl IntoView { } data["owner_id"] = serde_json::json!(owner_id.get()); } else { - if new_username.get().is_empty() || new_email.get().is_empty() || new_display_name.get().is_empty() { - set_message.set(Some(("Please fill in all new owner fields".to_string(), false))); + if new_username.get().is_empty() + || new_email.get().is_empty() + || new_display_name.get().is_empty() + { + set_message.set(Some(( + "Please fill in all new owner fields".to_string(), + false, + ))); set_pending.set(false); return; } @@ -111,7 +117,8 @@ pub fn RealmNewPage() -> impl IntoView { if let Ok(result) = resp.json::().await { set_created_slug.set(Some(result.slug)); set_temp_password.set(result.owner_temporary_password); - set_message.set(Some(("Realm created successfully!".to_string(), true))); + set_message + .set(Some(("Realm created successfully!".to_string(), true))); } } Ok(resp) => { diff --git a/crates/chattyness-admin-ui/src/pages/scene_detail.rs b/crates/chattyness-admin-ui/src/pages/scene_detail.rs index dabd5fc..7016fc2 100644 --- a/crates/chattyness-admin-ui/src/pages/scene_detail.rs +++ b/crates/chattyness-admin-ui/src/pages/scene_detail.rs @@ -1,9 +1,9 @@ //! Scene detail/edit page component. use leptos::prelude::*; -use leptos_router::hooks::use_params_map; #[cfg(feature = "hydrate")] use leptos::task::spawn_local; +use leptos_router::hooks::use_params_map; use uuid::Uuid; use crate::components::{Card, DetailGrid, DetailItem, PageHeader}; @@ -74,7 +74,9 @@ pub fn SceneDetailPage() -> impl IntoView { #[cfg(feature = "hydrate")] { use gloo_net::http::Request; - let resp = Request::get(&format!("/api/admin/scenes/{}", id)).send().await; + let resp = Request::get(&format!("/api/admin/scenes/{}", id)) + .send() + .await; match resp { Ok(r) if r.ok() => r.json::().await.ok(), _ => None, @@ -153,7 +155,10 @@ fn SceneDetailView( let (name, set_name) = signal(scene.name.clone()); let (description, set_description) = signal(scene.description.clone().unwrap_or_default()); let (background_color, set_background_color) = signal( - scene.background_color.clone().unwrap_or_else(|| "#1a1a2e".to_string()), + scene + .background_color + .clone() + .unwrap_or_else(|| "#1a1a2e".to_string()), ); let (background_image_url, set_background_image_url) = signal(String::new()); let (clear_background_image, set_clear_background_image) = signal(false); @@ -257,7 +262,6 @@ fn SceneDetailView( } }; - view! {
diff --git a/crates/chattyness-admin-ui/src/pages/scene_new.rs b/crates/chattyness-admin-ui/src/pages/scene_new.rs index 0927c20..32dee1a 100644 --- a/crates/chattyness-admin-ui/src/pages/scene_new.rs +++ b/crates/chattyness-admin-ui/src/pages/scene_new.rs @@ -1,9 +1,9 @@ //! Create new scene page component. use leptos::prelude::*; -use leptos_router::hooks::use_params_map; #[cfg(feature = "hydrate")] use leptos::task::spawn_local; +use leptos_router::hooks::use_params_map; use crate::components::{Card, PageHeader}; #[cfg(feature = "hydrate")] @@ -106,11 +106,7 @@ pub fn SceneNewPage() -> impl IntoView { spawn_local(async move { let url = format!("/api/admin/realms/{}/scenes", realm_slug_val); - let response = Request::post(&url) - .json(&data) - .unwrap() - .send() - .await; + let response = Request::post(&url).json(&data).unwrap().send().await; set_pending.set(false); @@ -124,7 +120,8 @@ pub fn SceneNewPage() -> impl IntoView { } if let Ok(result) = resp.json::().await { set_created_id.set(Some(result.id)); - set_message.set(Some(("Scene created successfully!".to_string(), true))); + set_message + .set(Some(("Scene created successfully!".to_string(), true))); } } Ok(resp) => { diff --git a/crates/chattyness-admin-ui/src/pages/staff.rs b/crates/chattyness-admin-ui/src/pages/staff.rs index dae2c10..fe72a71 100644 --- a/crates/chattyness-admin-ui/src/pages/staff.rs +++ b/crates/chattyness-admin-ui/src/pages/staff.rs @@ -205,10 +205,7 @@ fn AddStaffButton(message: RwSignal>) -> impl IntoView { #[component] #[allow(unused_variables)] -fn RemoveStaffButton( - user_id: String, - message: RwSignal>, -) -> impl IntoView { +fn RemoveStaffButton(user_id: String, message: RwSignal>) -> impl IntoView { let (pending, set_pending) = signal(false); #[cfg(feature = "hydrate")] let user_id_for_click = user_id.clone(); diff --git a/crates/chattyness-admin-ui/src/pages/user_detail.rs b/crates/chattyness-admin-ui/src/pages/user_detail.rs index 8ed83c5..48ea367 100644 --- a/crates/chattyness-admin-ui/src/pages/user_detail.rs +++ b/crates/chattyness-admin-ui/src/pages/user_detail.rs @@ -1,11 +1,13 @@ //! User detail page component. use leptos::prelude::*; -use leptos_router::hooks::use_params_map; #[cfg(feature = "hydrate")] use leptos::task::spawn_local; +use leptos_router::hooks::use_params_map; -use crate::components::{Card, DetailGrid, DetailItem, MessageAlert, PageHeader, StatusBadge, TempPasswordDisplay}; +use crate::components::{ + Card, DetailGrid, DetailItem, MessageAlert, PageHeader, StatusBadge, TempPasswordDisplay, +}; use crate::hooks::use_fetch_if; use crate::models::UserDetail; #[cfg(feature = "hydrate")] @@ -114,9 +116,10 @@ fn UserDetailView( let user_id = user_id_for_reset.clone(); spawn_local(async move { - let response = Request::post(&format!("/api/admin/users/{}/reset-password", user_id)) - .send() - .await; + let response = + Request::post(&format!("/api/admin/users/{}/reset-password", user_id)) + .send() + .await; set_pending_reset.set(false); @@ -128,7 +131,8 @@ fn UserDetailView( } if let Ok(result) = resp.json::().await { set_new_password.set(Some(result.temporary_password)); - set_message.set(Some(("Password reset successfully!".to_string(), true))); + set_message + .set(Some(("Password reset successfully!".to_string(), true))); } } _ => { diff --git a/crates/chattyness-admin-ui/src/routes.rs b/crates/chattyness-admin-ui/src/routes.rs index 0cd3d69..6c6d9fc 100644 --- a/crates/chattyness-admin-ui/src/routes.rs +++ b/crates/chattyness-admin-ui/src/routes.rs @@ -9,8 +9,8 @@ use leptos::prelude::*; use leptos_router::{ - components::{Route, Routes}, ParamSegment, StaticSegment, + components::{Route, Routes}, }; use crate::components::{AuthenticatedLayout, LoginLayout}; diff --git a/crates/chattyness-admin-ui/src/utils.rs b/crates/chattyness-admin-ui/src/utils.rs index 284f35c..77c37a2 100644 --- a/crates/chattyness-admin-ui/src/utils.rs +++ b/crates/chattyness-admin-ui/src/utils.rs @@ -128,14 +128,13 @@ pub fn fetch_image_dimensions_client( on_success: F, on_error: E, set_loading: leptos::prelude::WriteSignal, -) -where +) where F: Fn(u32, u32) + 'static, E: Fn(String) + Clone + 'static, { use leptos::prelude::Set; - use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; + use wasm_bindgen::prelude::*; let on_error_for_onerror = on_error.clone(); diff --git a/crates/chattyness-db/src/lib.rs b/crates/chattyness-db/src/lib.rs index 731bc5f..2bb9f9d 100644 --- a/crates/chattyness-db/src/lib.rs +++ b/crates/chattyness-db/src/lib.rs @@ -11,6 +11,6 @@ pub mod pool; pub mod queries; pub use models::*; -pub use ws_messages::*; #[cfg(feature = "ssr")] pub use pool::*; +pub use ws_messages::*; diff --git a/crates/chattyness-db/src/models.rs b/crates/chattyness-db/src/models.rs index 0d60a8b..9c4ac98 100644 --- a/crates/chattyness-db/src/models.rs +++ b/crates/chattyness-db/src/models.rs @@ -18,7 +18,10 @@ use chattyness_shared::validation; /// Realm privacy setting. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "ssr", derive(sqlx::Type))] -#[cfg_attr(feature = "ssr", sqlx(type_name = "realm_privacy", rename_all = "lowercase"))] +#[cfg_attr( + feature = "ssr", + sqlx(type_name = "realm_privacy", rename_all = "lowercase") +)] #[serde(rename_all = "lowercase")] pub enum RealmPrivacy { #[default] @@ -64,7 +67,10 @@ impl RealmPrivacy { /// Server-wide reputation tier. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "ssr", derive(sqlx::Type))] -#[cfg_attr(feature = "ssr", sqlx(type_name = "reputation_tier", rename_all = "lowercase"))] +#[cfg_attr( + feature = "ssr", + sqlx(type_name = "reputation_tier", rename_all = "lowercase") +)] #[serde(rename_all = "lowercase")] pub enum ReputationTier { Guest, @@ -78,7 +84,10 @@ pub enum ReputationTier { /// User account status. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "ssr", derive(sqlx::Type))] -#[cfg_attr(feature = "ssr", sqlx(type_name = "account_status", rename_all = "lowercase"))] +#[cfg_attr( + feature = "ssr", + sqlx(type_name = "account_status", rename_all = "lowercase") +)] #[serde(rename_all = "lowercase")] pub enum AccountStatus { #[default] @@ -91,7 +100,10 @@ pub enum AccountStatus { /// User account tag for feature gating and access control. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "ssr", derive(sqlx::Type))] -#[cfg_attr(feature = "ssr", sqlx(type_name = "user_tag", rename_all = "snake_case"))] +#[cfg_attr( + feature = "ssr", + sqlx(type_name = "user_tag", rename_all = "snake_case") +)] #[serde(rename_all = "snake_case")] pub enum UserTag { Guest, @@ -105,7 +117,10 @@ pub enum UserTag { /// Authentication provider. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "ssr", derive(sqlx::Type))] -#[cfg_attr(feature = "ssr", sqlx(type_name = "auth_provider", rename_all = "snake_case"))] +#[cfg_attr( + feature = "ssr", + sqlx(type_name = "auth_provider", rename_all = "snake_case") +)] #[serde(rename_all = "snake_case")] pub enum AuthProvider { #[default] @@ -118,7 +133,10 @@ pub enum AuthProvider { /// Server-level staff role. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "ssr", derive(sqlx::Type))] -#[cfg_attr(feature = "ssr", sqlx(type_name = "server_role", rename_all = "lowercase"))] +#[cfg_attr( + feature = "ssr", + sqlx(type_name = "server_role", rename_all = "lowercase") +)] #[serde(rename_all = "lowercase")] pub enum ServerRole { #[default] @@ -153,7 +171,10 @@ impl std::str::FromStr for ServerRole { /// Realm membership role. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "ssr", derive(sqlx::Type))] -#[cfg_attr(feature = "ssr", sqlx(type_name = "realm_role", rename_all = "lowercase"))] +#[cfg_attr( + feature = "ssr", + sqlx(type_name = "realm_role", rename_all = "lowercase") +)] #[serde(rename_all = "lowercase")] pub enum RealmRole { #[default] @@ -191,7 +212,10 @@ impl std::str::FromStr for RealmRole { /// Scene dimension mode. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "ssr", derive(sqlx::Type))] -#[cfg_attr(feature = "ssr", sqlx(type_name = "dimension_mode", rename_all = "lowercase"))] +#[cfg_attr( + feature = "ssr", + sqlx(type_name = "dimension_mode", rename_all = "lowercase") +)] #[serde(rename_all = "lowercase")] pub enum DimensionMode { #[default] @@ -223,7 +247,10 @@ impl std::str::FromStr for DimensionMode { /// Interactive spot type. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "ssr", derive(sqlx::Type))] -#[cfg_attr(feature = "ssr", sqlx(type_name = "spot_type", rename_all = "lowercase"))] +#[cfg_attr( + feature = "ssr", + sqlx(type_name = "spot_type", rename_all = "lowercase") +)] #[serde(rename_all = "lowercase")] pub enum SpotType { #[default] @@ -258,7 +285,10 @@ impl std::str::FromStr for SpotType { /// Avatar layer for prop positioning (z-depth). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "ssr", derive(sqlx::Type))] -#[cfg_attr(feature = "ssr", sqlx(type_name = "avatar_layer", rename_all = "lowercase"))] +#[cfg_attr( + feature = "ssr", + sqlx(type_name = "avatar_layer", rename_all = "lowercase") +)] #[serde(rename_all = "lowercase")] pub enum AvatarLayer { Skin, @@ -298,7 +328,10 @@ impl std::str::FromStr for AvatarLayer { /// - e10: sleeping, e11: wink #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "ssr", derive(sqlx::Type))] -#[cfg_attr(feature = "ssr", sqlx(type_name = "emotion_state", rename_all = "lowercase"))] +#[cfg_attr( + feature = "ssr", + sqlx(type_name = "emotion_state", rename_all = "lowercase") +)] #[serde(rename_all = "lowercase")] pub enum EmotionState { #[default] @@ -600,7 +633,10 @@ pub struct SpotSummary { /// Origin source for a prop in inventory. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "ssr", derive(sqlx::Type))] -#[cfg_attr(feature = "ssr", sqlx(type_name = "prop_origin", rename_all = "snake_case"))] +#[cfg_attr( + feature = "ssr", + sqlx(type_name = "prop_origin", rename_all = "snake_case") +)] #[serde(rename_all = "snake_case")] pub enum PropOrigin { #[default] @@ -773,7 +809,8 @@ impl CreateServerPropRequest { && self.default_position.is_none() { return Err(AppError::Validation( - "default_position is required when default_layer or default_emotion is set".to_string(), + "default_position is required when default_layer or default_emotion is set" + .to_string(), )); } Ok(()) diff --git a/crates/chattyness-db/src/pool.rs b/crates/chattyness-db/src/pool.rs index 2c70559..09d3575 100644 --- a/crates/chattyness-db/src/pool.rs +++ b/crates/chattyness-db/src/pool.rs @@ -2,7 +2,7 @@ use std::time::Duration; -use sqlx::{postgres::PgPoolOptions, PgPool}; +use sqlx::{PgPool, postgres::PgPoolOptions}; use uuid::Uuid; use chattyness_error::AppError; diff --git a/crates/chattyness-db/src/queries/avatars.rs b/crates/chattyness-db/src/queries/avatars.rs index f13757a..19fed8a 100644 --- a/crates/chattyness-db/src/queries/avatars.rs +++ b/crates/chattyness-db/src/queries/avatars.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; -use sqlx::{postgres::PgConnection, PgExecutor, PgPool}; +use sqlx::{PgExecutor, PgPool, postgres::PgConnection}; use uuid::Uuid; use crate::models::{ActiveAvatar, AvatarWithPaths, EmotionAvailability, EmotionState}; @@ -312,83 +312,218 @@ pub async fn get_avatar_with_paths( let mut uuids: Vec = Vec::new(); // Content layers - collect_uuids(&mut uuids, &[ - avatar.l_skin_0, avatar.l_skin_1, avatar.l_skin_2, - avatar.l_skin_3, avatar.l_skin_4, avatar.l_skin_5, - avatar.l_skin_6, avatar.l_skin_7, avatar.l_skin_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.l_clothes_0, avatar.l_clothes_1, avatar.l_clothes_2, - avatar.l_clothes_3, avatar.l_clothes_4, avatar.l_clothes_5, - avatar.l_clothes_6, avatar.l_clothes_7, avatar.l_clothes_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.l_accessories_0, avatar.l_accessories_1, avatar.l_accessories_2, - avatar.l_accessories_3, avatar.l_accessories_4, avatar.l_accessories_5, - avatar.l_accessories_6, avatar.l_accessories_7, avatar.l_accessories_8, - ]); + collect_uuids( + &mut uuids, + &[ + avatar.l_skin_0, + avatar.l_skin_1, + avatar.l_skin_2, + avatar.l_skin_3, + avatar.l_skin_4, + avatar.l_skin_5, + avatar.l_skin_6, + avatar.l_skin_7, + avatar.l_skin_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.l_clothes_0, + avatar.l_clothes_1, + avatar.l_clothes_2, + avatar.l_clothes_3, + avatar.l_clothes_4, + avatar.l_clothes_5, + avatar.l_clothes_6, + avatar.l_clothes_7, + avatar.l_clothes_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.l_accessories_0, + avatar.l_accessories_1, + avatar.l_accessories_2, + avatar.l_accessories_3, + avatar.l_accessories_4, + avatar.l_accessories_5, + avatar.l_accessories_6, + avatar.l_accessories_7, + avatar.l_accessories_8, + ], + ); // Emotion layers (12 emotions × 9 positions) - collect_uuids(&mut uuids, &[ - avatar.e_neutral_0, avatar.e_neutral_1, avatar.e_neutral_2, - avatar.e_neutral_3, avatar.e_neutral_4, avatar.e_neutral_5, - avatar.e_neutral_6, avatar.e_neutral_7, avatar.e_neutral_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.e_happy_0, avatar.e_happy_1, avatar.e_happy_2, - avatar.e_happy_3, avatar.e_happy_4, avatar.e_happy_5, - avatar.e_happy_6, avatar.e_happy_7, avatar.e_happy_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.e_sad_0, avatar.e_sad_1, avatar.e_sad_2, - avatar.e_sad_3, avatar.e_sad_4, avatar.e_sad_5, - avatar.e_sad_6, avatar.e_sad_7, avatar.e_sad_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.e_angry_0, avatar.e_angry_1, avatar.e_angry_2, - avatar.e_angry_3, avatar.e_angry_4, avatar.e_angry_5, - avatar.e_angry_6, avatar.e_angry_7, avatar.e_angry_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.e_surprised_0, avatar.e_surprised_1, avatar.e_surprised_2, - avatar.e_surprised_3, avatar.e_surprised_4, avatar.e_surprised_5, - avatar.e_surprised_6, avatar.e_surprised_7, avatar.e_surprised_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.e_thinking_0, avatar.e_thinking_1, avatar.e_thinking_2, - avatar.e_thinking_3, avatar.e_thinking_4, avatar.e_thinking_5, - avatar.e_thinking_6, avatar.e_thinking_7, avatar.e_thinking_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.e_laughing_0, avatar.e_laughing_1, avatar.e_laughing_2, - avatar.e_laughing_3, avatar.e_laughing_4, avatar.e_laughing_5, - avatar.e_laughing_6, avatar.e_laughing_7, avatar.e_laughing_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.e_crying_0, avatar.e_crying_1, avatar.e_crying_2, - avatar.e_crying_3, avatar.e_crying_4, avatar.e_crying_5, - avatar.e_crying_6, avatar.e_crying_7, avatar.e_crying_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.e_love_0, avatar.e_love_1, avatar.e_love_2, - avatar.e_love_3, avatar.e_love_4, avatar.e_love_5, - avatar.e_love_6, avatar.e_love_7, avatar.e_love_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.e_confused_0, avatar.e_confused_1, avatar.e_confused_2, - avatar.e_confused_3, avatar.e_confused_4, avatar.e_confused_5, - avatar.e_confused_6, avatar.e_confused_7, avatar.e_confused_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.e_sleeping_0, avatar.e_sleeping_1, avatar.e_sleeping_2, - avatar.e_sleeping_3, avatar.e_sleeping_4, avatar.e_sleeping_5, - avatar.e_sleeping_6, avatar.e_sleeping_7, avatar.e_sleeping_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.e_wink_0, avatar.e_wink_1, avatar.e_wink_2, - avatar.e_wink_3, avatar.e_wink_4, avatar.e_wink_5, - avatar.e_wink_6, avatar.e_wink_7, avatar.e_wink_8, - ]); + collect_uuids( + &mut uuids, + &[ + avatar.e_neutral_0, + avatar.e_neutral_1, + avatar.e_neutral_2, + avatar.e_neutral_3, + avatar.e_neutral_4, + avatar.e_neutral_5, + avatar.e_neutral_6, + avatar.e_neutral_7, + avatar.e_neutral_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.e_happy_0, + avatar.e_happy_1, + avatar.e_happy_2, + avatar.e_happy_3, + avatar.e_happy_4, + avatar.e_happy_5, + avatar.e_happy_6, + avatar.e_happy_7, + avatar.e_happy_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.e_sad_0, + avatar.e_sad_1, + avatar.e_sad_2, + avatar.e_sad_3, + avatar.e_sad_4, + avatar.e_sad_5, + avatar.e_sad_6, + avatar.e_sad_7, + avatar.e_sad_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.e_angry_0, + avatar.e_angry_1, + avatar.e_angry_2, + avatar.e_angry_3, + avatar.e_angry_4, + avatar.e_angry_5, + avatar.e_angry_6, + avatar.e_angry_7, + avatar.e_angry_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.e_surprised_0, + avatar.e_surprised_1, + avatar.e_surprised_2, + avatar.e_surprised_3, + avatar.e_surprised_4, + avatar.e_surprised_5, + avatar.e_surprised_6, + avatar.e_surprised_7, + avatar.e_surprised_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.e_thinking_0, + avatar.e_thinking_1, + avatar.e_thinking_2, + avatar.e_thinking_3, + avatar.e_thinking_4, + avatar.e_thinking_5, + avatar.e_thinking_6, + avatar.e_thinking_7, + avatar.e_thinking_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.e_laughing_0, + avatar.e_laughing_1, + avatar.e_laughing_2, + avatar.e_laughing_3, + avatar.e_laughing_4, + avatar.e_laughing_5, + avatar.e_laughing_6, + avatar.e_laughing_7, + avatar.e_laughing_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.e_crying_0, + avatar.e_crying_1, + avatar.e_crying_2, + avatar.e_crying_3, + avatar.e_crying_4, + avatar.e_crying_5, + avatar.e_crying_6, + avatar.e_crying_7, + avatar.e_crying_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.e_love_0, + avatar.e_love_1, + avatar.e_love_2, + avatar.e_love_3, + avatar.e_love_4, + avatar.e_love_5, + avatar.e_love_6, + avatar.e_love_7, + avatar.e_love_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.e_confused_0, + avatar.e_confused_1, + avatar.e_confused_2, + avatar.e_confused_3, + avatar.e_confused_4, + avatar.e_confused_5, + avatar.e_confused_6, + avatar.e_confused_7, + avatar.e_confused_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.e_sleeping_0, + avatar.e_sleeping_1, + avatar.e_sleeping_2, + avatar.e_sleeping_3, + avatar.e_sleeping_4, + avatar.e_sleeping_5, + avatar.e_sleeping_6, + avatar.e_sleeping_7, + avatar.e_sleeping_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.e_wink_0, + avatar.e_wink_1, + avatar.e_wink_2, + avatar.e_wink_3, + avatar.e_wink_4, + avatar.e_wink_5, + avatar.e_wink_6, + avatar.e_wink_7, + avatar.e_wink_8, + ], + ); // Query 2: Bulk resolve all UUIDs to paths let paths: HashMap = if uuids.is_empty() { @@ -405,121 +540,328 @@ pub async fn get_avatar_with_paths( }; // Build the AvatarWithPaths - let resolve = |uuid: Option| -> Option { - uuid.and_then(|id| paths.get(&id).cloned()) - }; + let resolve = + |uuid: Option| -> Option { uuid.and_then(|id| paths.get(&id).cloned()) }; // Check if any UUID in the array is non-null (emotion is available) - let has_any = |slots: &[Option]| -> bool { - slots.iter().any(|u| u.is_some()) - }; + let has_any = |slots: &[Option]| -> bool { slots.iter().any(|u| u.is_some()) }; // Compute emotions_available from UUID presence (not path resolution) let emotions_available = [ - has_any(&[avatar.e_neutral_0, avatar.e_neutral_1, avatar.e_neutral_2, avatar.e_neutral_3, avatar.e_neutral_4, avatar.e_neutral_5, avatar.e_neutral_6, avatar.e_neutral_7, avatar.e_neutral_8]), - has_any(&[avatar.e_happy_0, avatar.e_happy_1, avatar.e_happy_2, avatar.e_happy_3, avatar.e_happy_4, avatar.e_happy_5, avatar.e_happy_6, avatar.e_happy_7, avatar.e_happy_8]), - has_any(&[avatar.e_sad_0, avatar.e_sad_1, avatar.e_sad_2, avatar.e_sad_3, avatar.e_sad_4, avatar.e_sad_5, avatar.e_sad_6, avatar.e_sad_7, avatar.e_sad_8]), - has_any(&[avatar.e_angry_0, avatar.e_angry_1, avatar.e_angry_2, avatar.e_angry_3, avatar.e_angry_4, avatar.e_angry_5, avatar.e_angry_6, avatar.e_angry_7, avatar.e_angry_8]), - has_any(&[avatar.e_surprised_0, avatar.e_surprised_1, avatar.e_surprised_2, avatar.e_surprised_3, avatar.e_surprised_4, avatar.e_surprised_5, avatar.e_surprised_6, avatar.e_surprised_7, avatar.e_surprised_8]), - has_any(&[avatar.e_thinking_0, avatar.e_thinking_1, avatar.e_thinking_2, avatar.e_thinking_3, avatar.e_thinking_4, avatar.e_thinking_5, avatar.e_thinking_6, avatar.e_thinking_7, avatar.e_thinking_8]), - has_any(&[avatar.e_laughing_0, avatar.e_laughing_1, avatar.e_laughing_2, avatar.e_laughing_3, avatar.e_laughing_4, avatar.e_laughing_5, avatar.e_laughing_6, avatar.e_laughing_7, avatar.e_laughing_8]), - has_any(&[avatar.e_crying_0, avatar.e_crying_1, avatar.e_crying_2, avatar.e_crying_3, avatar.e_crying_4, avatar.e_crying_5, avatar.e_crying_6, avatar.e_crying_7, avatar.e_crying_8]), - has_any(&[avatar.e_love_0, avatar.e_love_1, avatar.e_love_2, avatar.e_love_3, avatar.e_love_4, avatar.e_love_5, avatar.e_love_6, avatar.e_love_7, avatar.e_love_8]), - has_any(&[avatar.e_confused_0, avatar.e_confused_1, avatar.e_confused_2, avatar.e_confused_3, avatar.e_confused_4, avatar.e_confused_5, avatar.e_confused_6, avatar.e_confused_7, avatar.e_confused_8]), - has_any(&[avatar.e_sleeping_0, avatar.e_sleeping_1, avatar.e_sleeping_2, avatar.e_sleeping_3, avatar.e_sleeping_4, avatar.e_sleeping_5, avatar.e_sleeping_6, avatar.e_sleeping_7, avatar.e_sleeping_8]), - has_any(&[avatar.e_wink_0, avatar.e_wink_1, avatar.e_wink_2, avatar.e_wink_3, avatar.e_wink_4, avatar.e_wink_5, avatar.e_wink_6, avatar.e_wink_7, avatar.e_wink_8]), + has_any(&[ + avatar.e_neutral_0, + avatar.e_neutral_1, + avatar.e_neutral_2, + avatar.e_neutral_3, + avatar.e_neutral_4, + avatar.e_neutral_5, + avatar.e_neutral_6, + avatar.e_neutral_7, + avatar.e_neutral_8, + ]), + has_any(&[ + avatar.e_happy_0, + avatar.e_happy_1, + avatar.e_happy_2, + avatar.e_happy_3, + avatar.e_happy_4, + avatar.e_happy_5, + avatar.e_happy_6, + avatar.e_happy_7, + avatar.e_happy_8, + ]), + has_any(&[ + avatar.e_sad_0, + avatar.e_sad_1, + avatar.e_sad_2, + avatar.e_sad_3, + avatar.e_sad_4, + avatar.e_sad_5, + avatar.e_sad_6, + avatar.e_sad_7, + avatar.e_sad_8, + ]), + has_any(&[ + avatar.e_angry_0, + avatar.e_angry_1, + avatar.e_angry_2, + avatar.e_angry_3, + avatar.e_angry_4, + avatar.e_angry_5, + avatar.e_angry_6, + avatar.e_angry_7, + avatar.e_angry_8, + ]), + has_any(&[ + avatar.e_surprised_0, + avatar.e_surprised_1, + avatar.e_surprised_2, + avatar.e_surprised_3, + avatar.e_surprised_4, + avatar.e_surprised_5, + avatar.e_surprised_6, + avatar.e_surprised_7, + avatar.e_surprised_8, + ]), + has_any(&[ + avatar.e_thinking_0, + avatar.e_thinking_1, + avatar.e_thinking_2, + avatar.e_thinking_3, + avatar.e_thinking_4, + avatar.e_thinking_5, + avatar.e_thinking_6, + avatar.e_thinking_7, + avatar.e_thinking_8, + ]), + has_any(&[ + avatar.e_laughing_0, + avatar.e_laughing_1, + avatar.e_laughing_2, + avatar.e_laughing_3, + avatar.e_laughing_4, + avatar.e_laughing_5, + avatar.e_laughing_6, + avatar.e_laughing_7, + avatar.e_laughing_8, + ]), + has_any(&[ + avatar.e_crying_0, + avatar.e_crying_1, + avatar.e_crying_2, + avatar.e_crying_3, + avatar.e_crying_4, + avatar.e_crying_5, + avatar.e_crying_6, + avatar.e_crying_7, + avatar.e_crying_8, + ]), + has_any(&[ + avatar.e_love_0, + avatar.e_love_1, + avatar.e_love_2, + avatar.e_love_3, + avatar.e_love_4, + avatar.e_love_5, + avatar.e_love_6, + avatar.e_love_7, + avatar.e_love_8, + ]), + has_any(&[ + avatar.e_confused_0, + avatar.e_confused_1, + avatar.e_confused_2, + avatar.e_confused_3, + avatar.e_confused_4, + avatar.e_confused_5, + avatar.e_confused_6, + avatar.e_confused_7, + avatar.e_confused_8, + ]), + has_any(&[ + avatar.e_sleeping_0, + avatar.e_sleeping_1, + avatar.e_sleeping_2, + avatar.e_sleeping_3, + avatar.e_sleeping_4, + avatar.e_sleeping_5, + avatar.e_sleeping_6, + avatar.e_sleeping_7, + avatar.e_sleeping_8, + ]), + has_any(&[ + avatar.e_wink_0, + avatar.e_wink_1, + avatar.e_wink_2, + avatar.e_wink_3, + avatar.e_wink_4, + avatar.e_wink_5, + avatar.e_wink_6, + avatar.e_wink_7, + avatar.e_wink_8, + ]), ]; Ok(Some(AvatarWithPaths { avatar_id: avatar.id, current_emotion: avatar.current_emotion, skin_layer: [ - resolve(avatar.l_skin_0), resolve(avatar.l_skin_1), resolve(avatar.l_skin_2), - resolve(avatar.l_skin_3), resolve(avatar.l_skin_4), resolve(avatar.l_skin_5), - resolve(avatar.l_skin_6), resolve(avatar.l_skin_7), resolve(avatar.l_skin_8), + resolve(avatar.l_skin_0), + resolve(avatar.l_skin_1), + resolve(avatar.l_skin_2), + resolve(avatar.l_skin_3), + resolve(avatar.l_skin_4), + resolve(avatar.l_skin_5), + resolve(avatar.l_skin_6), + resolve(avatar.l_skin_7), + resolve(avatar.l_skin_8), ], clothes_layer: [ - resolve(avatar.l_clothes_0), resolve(avatar.l_clothes_1), resolve(avatar.l_clothes_2), - resolve(avatar.l_clothes_3), resolve(avatar.l_clothes_4), resolve(avatar.l_clothes_5), - resolve(avatar.l_clothes_6), resolve(avatar.l_clothes_7), resolve(avatar.l_clothes_8), + resolve(avatar.l_clothes_0), + resolve(avatar.l_clothes_1), + resolve(avatar.l_clothes_2), + resolve(avatar.l_clothes_3), + resolve(avatar.l_clothes_4), + resolve(avatar.l_clothes_5), + resolve(avatar.l_clothes_6), + resolve(avatar.l_clothes_7), + resolve(avatar.l_clothes_8), ], accessories_layer: [ - resolve(avatar.l_accessories_0), resolve(avatar.l_accessories_1), resolve(avatar.l_accessories_2), - resolve(avatar.l_accessories_3), resolve(avatar.l_accessories_4), resolve(avatar.l_accessories_5), - resolve(avatar.l_accessories_6), resolve(avatar.l_accessories_7), resolve(avatar.l_accessories_8), + resolve(avatar.l_accessories_0), + resolve(avatar.l_accessories_1), + resolve(avatar.l_accessories_2), + resolve(avatar.l_accessories_3), + resolve(avatar.l_accessories_4), + resolve(avatar.l_accessories_5), + resolve(avatar.l_accessories_6), + resolve(avatar.l_accessories_7), + resolve(avatar.l_accessories_8), ], emotions: [ // Neutral (0) [ - resolve(avatar.e_neutral_0), resolve(avatar.e_neutral_1), resolve(avatar.e_neutral_2), - resolve(avatar.e_neutral_3), resolve(avatar.e_neutral_4), resolve(avatar.e_neutral_5), - resolve(avatar.e_neutral_6), resolve(avatar.e_neutral_7), resolve(avatar.e_neutral_8), + resolve(avatar.e_neutral_0), + resolve(avatar.e_neutral_1), + resolve(avatar.e_neutral_2), + resolve(avatar.e_neutral_3), + resolve(avatar.e_neutral_4), + resolve(avatar.e_neutral_5), + resolve(avatar.e_neutral_6), + resolve(avatar.e_neutral_7), + resolve(avatar.e_neutral_8), ], // Happy (1) [ - resolve(avatar.e_happy_0), resolve(avatar.e_happy_1), resolve(avatar.e_happy_2), - resolve(avatar.e_happy_3), resolve(avatar.e_happy_4), resolve(avatar.e_happy_5), - resolve(avatar.e_happy_6), resolve(avatar.e_happy_7), resolve(avatar.e_happy_8), + resolve(avatar.e_happy_0), + resolve(avatar.e_happy_1), + resolve(avatar.e_happy_2), + resolve(avatar.e_happy_3), + resolve(avatar.e_happy_4), + resolve(avatar.e_happy_5), + resolve(avatar.e_happy_6), + resolve(avatar.e_happy_7), + resolve(avatar.e_happy_8), ], // Sad (2) [ - resolve(avatar.e_sad_0), resolve(avatar.e_sad_1), resolve(avatar.e_sad_2), - resolve(avatar.e_sad_3), resolve(avatar.e_sad_4), resolve(avatar.e_sad_5), - resolve(avatar.e_sad_6), resolve(avatar.e_sad_7), resolve(avatar.e_sad_8), + resolve(avatar.e_sad_0), + resolve(avatar.e_sad_1), + resolve(avatar.e_sad_2), + resolve(avatar.e_sad_3), + resolve(avatar.e_sad_4), + resolve(avatar.e_sad_5), + resolve(avatar.e_sad_6), + resolve(avatar.e_sad_7), + resolve(avatar.e_sad_8), ], // Angry (3) [ - resolve(avatar.e_angry_0), resolve(avatar.e_angry_1), resolve(avatar.e_angry_2), - resolve(avatar.e_angry_3), resolve(avatar.e_angry_4), resolve(avatar.e_angry_5), - resolve(avatar.e_angry_6), resolve(avatar.e_angry_7), resolve(avatar.e_angry_8), + resolve(avatar.e_angry_0), + resolve(avatar.e_angry_1), + resolve(avatar.e_angry_2), + resolve(avatar.e_angry_3), + resolve(avatar.e_angry_4), + resolve(avatar.e_angry_5), + resolve(avatar.e_angry_6), + resolve(avatar.e_angry_7), + resolve(avatar.e_angry_8), ], // Surprised (4) [ - resolve(avatar.e_surprised_0), resolve(avatar.e_surprised_1), resolve(avatar.e_surprised_2), - resolve(avatar.e_surprised_3), resolve(avatar.e_surprised_4), resolve(avatar.e_surprised_5), - resolve(avatar.e_surprised_6), resolve(avatar.e_surprised_7), resolve(avatar.e_surprised_8), + resolve(avatar.e_surprised_0), + resolve(avatar.e_surprised_1), + resolve(avatar.e_surprised_2), + resolve(avatar.e_surprised_3), + resolve(avatar.e_surprised_4), + resolve(avatar.e_surprised_5), + resolve(avatar.e_surprised_6), + resolve(avatar.e_surprised_7), + resolve(avatar.e_surprised_8), ], // Thinking (5) [ - resolve(avatar.e_thinking_0), resolve(avatar.e_thinking_1), resolve(avatar.e_thinking_2), - resolve(avatar.e_thinking_3), resolve(avatar.e_thinking_4), resolve(avatar.e_thinking_5), - resolve(avatar.e_thinking_6), resolve(avatar.e_thinking_7), resolve(avatar.e_thinking_8), + resolve(avatar.e_thinking_0), + resolve(avatar.e_thinking_1), + resolve(avatar.e_thinking_2), + resolve(avatar.e_thinking_3), + resolve(avatar.e_thinking_4), + resolve(avatar.e_thinking_5), + resolve(avatar.e_thinking_6), + resolve(avatar.e_thinking_7), + resolve(avatar.e_thinking_8), ], // Laughing (6) [ - resolve(avatar.e_laughing_0), resolve(avatar.e_laughing_1), resolve(avatar.e_laughing_2), - resolve(avatar.e_laughing_3), resolve(avatar.e_laughing_4), resolve(avatar.e_laughing_5), - resolve(avatar.e_laughing_6), resolve(avatar.e_laughing_7), resolve(avatar.e_laughing_8), + resolve(avatar.e_laughing_0), + resolve(avatar.e_laughing_1), + resolve(avatar.e_laughing_2), + resolve(avatar.e_laughing_3), + resolve(avatar.e_laughing_4), + resolve(avatar.e_laughing_5), + resolve(avatar.e_laughing_6), + resolve(avatar.e_laughing_7), + resolve(avatar.e_laughing_8), ], // Crying (7) [ - resolve(avatar.e_crying_0), resolve(avatar.e_crying_1), resolve(avatar.e_crying_2), - resolve(avatar.e_crying_3), resolve(avatar.e_crying_4), resolve(avatar.e_crying_5), - resolve(avatar.e_crying_6), resolve(avatar.e_crying_7), resolve(avatar.e_crying_8), + resolve(avatar.e_crying_0), + resolve(avatar.e_crying_1), + resolve(avatar.e_crying_2), + resolve(avatar.e_crying_3), + resolve(avatar.e_crying_4), + resolve(avatar.e_crying_5), + resolve(avatar.e_crying_6), + resolve(avatar.e_crying_7), + resolve(avatar.e_crying_8), ], // Love (8) [ - resolve(avatar.e_love_0), resolve(avatar.e_love_1), resolve(avatar.e_love_2), - resolve(avatar.e_love_3), resolve(avatar.e_love_4), resolve(avatar.e_love_5), - resolve(avatar.e_love_6), resolve(avatar.e_love_7), resolve(avatar.e_love_8), + resolve(avatar.e_love_0), + resolve(avatar.e_love_1), + resolve(avatar.e_love_2), + resolve(avatar.e_love_3), + resolve(avatar.e_love_4), + resolve(avatar.e_love_5), + resolve(avatar.e_love_6), + resolve(avatar.e_love_7), + resolve(avatar.e_love_8), ], // Confused (9) [ - resolve(avatar.e_confused_0), resolve(avatar.e_confused_1), resolve(avatar.e_confused_2), - resolve(avatar.e_confused_3), resolve(avatar.e_confused_4), resolve(avatar.e_confused_5), - resolve(avatar.e_confused_6), resolve(avatar.e_confused_7), resolve(avatar.e_confused_8), + resolve(avatar.e_confused_0), + resolve(avatar.e_confused_1), + resolve(avatar.e_confused_2), + resolve(avatar.e_confused_3), + resolve(avatar.e_confused_4), + resolve(avatar.e_confused_5), + resolve(avatar.e_confused_6), + resolve(avatar.e_confused_7), + resolve(avatar.e_confused_8), ], // Sleeping (10) [ - resolve(avatar.e_sleeping_0), resolve(avatar.e_sleeping_1), resolve(avatar.e_sleeping_2), - resolve(avatar.e_sleeping_3), resolve(avatar.e_sleeping_4), resolve(avatar.e_sleeping_5), - resolve(avatar.e_sleeping_6), resolve(avatar.e_sleeping_7), resolve(avatar.e_sleeping_8), + resolve(avatar.e_sleeping_0), + resolve(avatar.e_sleeping_1), + resolve(avatar.e_sleeping_2), + resolve(avatar.e_sleeping_3), + resolve(avatar.e_sleeping_4), + resolve(avatar.e_sleeping_5), + resolve(avatar.e_sleeping_6), + resolve(avatar.e_sleeping_7), + resolve(avatar.e_sleeping_8), ], // Wink (11) [ - resolve(avatar.e_wink_0), resolve(avatar.e_wink_1), resolve(avatar.e_wink_2), - resolve(avatar.e_wink_3), resolve(avatar.e_wink_4), resolve(avatar.e_wink_5), - resolve(avatar.e_wink_6), resolve(avatar.e_wink_7), resolve(avatar.e_wink_8), + resolve(avatar.e_wink_0), + resolve(avatar.e_wink_1), + resolve(avatar.e_wink_2), + resolve(avatar.e_wink_3), + resolve(avatar.e_wink_4), + resolve(avatar.e_wink_5), + resolve(avatar.e_wink_6), + resolve(avatar.e_wink_7), + resolve(avatar.e_wink_8), ], ], emotions_available, @@ -558,83 +900,218 @@ pub async fn get_avatar_with_paths_conn( let mut uuids: Vec = Vec::new(); // Content layers - collect_uuids(&mut uuids, &[ - avatar.l_skin_0, avatar.l_skin_1, avatar.l_skin_2, - avatar.l_skin_3, avatar.l_skin_4, avatar.l_skin_5, - avatar.l_skin_6, avatar.l_skin_7, avatar.l_skin_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.l_clothes_0, avatar.l_clothes_1, avatar.l_clothes_2, - avatar.l_clothes_3, avatar.l_clothes_4, avatar.l_clothes_5, - avatar.l_clothes_6, avatar.l_clothes_7, avatar.l_clothes_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.l_accessories_0, avatar.l_accessories_1, avatar.l_accessories_2, - avatar.l_accessories_3, avatar.l_accessories_4, avatar.l_accessories_5, - avatar.l_accessories_6, avatar.l_accessories_7, avatar.l_accessories_8, - ]); + collect_uuids( + &mut uuids, + &[ + avatar.l_skin_0, + avatar.l_skin_1, + avatar.l_skin_2, + avatar.l_skin_3, + avatar.l_skin_4, + avatar.l_skin_5, + avatar.l_skin_6, + avatar.l_skin_7, + avatar.l_skin_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.l_clothes_0, + avatar.l_clothes_1, + avatar.l_clothes_2, + avatar.l_clothes_3, + avatar.l_clothes_4, + avatar.l_clothes_5, + avatar.l_clothes_6, + avatar.l_clothes_7, + avatar.l_clothes_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.l_accessories_0, + avatar.l_accessories_1, + avatar.l_accessories_2, + avatar.l_accessories_3, + avatar.l_accessories_4, + avatar.l_accessories_5, + avatar.l_accessories_6, + avatar.l_accessories_7, + avatar.l_accessories_8, + ], + ); // Emotion layers (12 emotions × 9 positions) - collect_uuids(&mut uuids, &[ - avatar.e_neutral_0, avatar.e_neutral_1, avatar.e_neutral_2, - avatar.e_neutral_3, avatar.e_neutral_4, avatar.e_neutral_5, - avatar.e_neutral_6, avatar.e_neutral_7, avatar.e_neutral_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.e_happy_0, avatar.e_happy_1, avatar.e_happy_2, - avatar.e_happy_3, avatar.e_happy_4, avatar.e_happy_5, - avatar.e_happy_6, avatar.e_happy_7, avatar.e_happy_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.e_sad_0, avatar.e_sad_1, avatar.e_sad_2, - avatar.e_sad_3, avatar.e_sad_4, avatar.e_sad_5, - avatar.e_sad_6, avatar.e_sad_7, avatar.e_sad_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.e_angry_0, avatar.e_angry_1, avatar.e_angry_2, - avatar.e_angry_3, avatar.e_angry_4, avatar.e_angry_5, - avatar.e_angry_6, avatar.e_angry_7, avatar.e_angry_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.e_surprised_0, avatar.e_surprised_1, avatar.e_surprised_2, - avatar.e_surprised_3, avatar.e_surprised_4, avatar.e_surprised_5, - avatar.e_surprised_6, avatar.e_surprised_7, avatar.e_surprised_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.e_thinking_0, avatar.e_thinking_1, avatar.e_thinking_2, - avatar.e_thinking_3, avatar.e_thinking_4, avatar.e_thinking_5, - avatar.e_thinking_6, avatar.e_thinking_7, avatar.e_thinking_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.e_laughing_0, avatar.e_laughing_1, avatar.e_laughing_2, - avatar.e_laughing_3, avatar.e_laughing_4, avatar.e_laughing_5, - avatar.e_laughing_6, avatar.e_laughing_7, avatar.e_laughing_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.e_crying_0, avatar.e_crying_1, avatar.e_crying_2, - avatar.e_crying_3, avatar.e_crying_4, avatar.e_crying_5, - avatar.e_crying_6, avatar.e_crying_7, avatar.e_crying_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.e_love_0, avatar.e_love_1, avatar.e_love_2, - avatar.e_love_3, avatar.e_love_4, avatar.e_love_5, - avatar.e_love_6, avatar.e_love_7, avatar.e_love_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.e_confused_0, avatar.e_confused_1, avatar.e_confused_2, - avatar.e_confused_3, avatar.e_confused_4, avatar.e_confused_5, - avatar.e_confused_6, avatar.e_confused_7, avatar.e_confused_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.e_sleeping_0, avatar.e_sleeping_1, avatar.e_sleeping_2, - avatar.e_sleeping_3, avatar.e_sleeping_4, avatar.e_sleeping_5, - avatar.e_sleeping_6, avatar.e_sleeping_7, avatar.e_sleeping_8, - ]); - collect_uuids(&mut uuids, &[ - avatar.e_wink_0, avatar.e_wink_1, avatar.e_wink_2, - avatar.e_wink_3, avatar.e_wink_4, avatar.e_wink_5, - avatar.e_wink_6, avatar.e_wink_7, avatar.e_wink_8, - ]); + collect_uuids( + &mut uuids, + &[ + avatar.e_neutral_0, + avatar.e_neutral_1, + avatar.e_neutral_2, + avatar.e_neutral_3, + avatar.e_neutral_4, + avatar.e_neutral_5, + avatar.e_neutral_6, + avatar.e_neutral_7, + avatar.e_neutral_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.e_happy_0, + avatar.e_happy_1, + avatar.e_happy_2, + avatar.e_happy_3, + avatar.e_happy_4, + avatar.e_happy_5, + avatar.e_happy_6, + avatar.e_happy_7, + avatar.e_happy_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.e_sad_0, + avatar.e_sad_1, + avatar.e_sad_2, + avatar.e_sad_3, + avatar.e_sad_4, + avatar.e_sad_5, + avatar.e_sad_6, + avatar.e_sad_7, + avatar.e_sad_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.e_angry_0, + avatar.e_angry_1, + avatar.e_angry_2, + avatar.e_angry_3, + avatar.e_angry_4, + avatar.e_angry_5, + avatar.e_angry_6, + avatar.e_angry_7, + avatar.e_angry_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.e_surprised_0, + avatar.e_surprised_1, + avatar.e_surprised_2, + avatar.e_surprised_3, + avatar.e_surprised_4, + avatar.e_surprised_5, + avatar.e_surprised_6, + avatar.e_surprised_7, + avatar.e_surprised_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.e_thinking_0, + avatar.e_thinking_1, + avatar.e_thinking_2, + avatar.e_thinking_3, + avatar.e_thinking_4, + avatar.e_thinking_5, + avatar.e_thinking_6, + avatar.e_thinking_7, + avatar.e_thinking_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.e_laughing_0, + avatar.e_laughing_1, + avatar.e_laughing_2, + avatar.e_laughing_3, + avatar.e_laughing_4, + avatar.e_laughing_5, + avatar.e_laughing_6, + avatar.e_laughing_7, + avatar.e_laughing_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.e_crying_0, + avatar.e_crying_1, + avatar.e_crying_2, + avatar.e_crying_3, + avatar.e_crying_4, + avatar.e_crying_5, + avatar.e_crying_6, + avatar.e_crying_7, + avatar.e_crying_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.e_love_0, + avatar.e_love_1, + avatar.e_love_2, + avatar.e_love_3, + avatar.e_love_4, + avatar.e_love_5, + avatar.e_love_6, + avatar.e_love_7, + avatar.e_love_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.e_confused_0, + avatar.e_confused_1, + avatar.e_confused_2, + avatar.e_confused_3, + avatar.e_confused_4, + avatar.e_confused_5, + avatar.e_confused_6, + avatar.e_confused_7, + avatar.e_confused_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.e_sleeping_0, + avatar.e_sleeping_1, + avatar.e_sleeping_2, + avatar.e_sleeping_3, + avatar.e_sleeping_4, + avatar.e_sleeping_5, + avatar.e_sleeping_6, + avatar.e_sleeping_7, + avatar.e_sleeping_8, + ], + ); + collect_uuids( + &mut uuids, + &[ + avatar.e_wink_0, + avatar.e_wink_1, + avatar.e_wink_2, + avatar.e_wink_3, + avatar.e_wink_4, + avatar.e_wink_5, + avatar.e_wink_6, + avatar.e_wink_7, + avatar.e_wink_8, + ], + ); // Query 2: Bulk resolve all UUIDs to paths let paths: HashMap = if uuids.is_empty() { @@ -651,121 +1128,328 @@ pub async fn get_avatar_with_paths_conn( }; // Build the AvatarWithPaths - let resolve = |uuid: Option| -> Option { - uuid.and_then(|id| paths.get(&id).cloned()) - }; + let resolve = + |uuid: Option| -> Option { uuid.and_then(|id| paths.get(&id).cloned()) }; // Check if any UUID in the array is non-null (emotion is available) - let has_any = |slots: &[Option]| -> bool { - slots.iter().any(|u| u.is_some()) - }; + let has_any = |slots: &[Option]| -> bool { slots.iter().any(|u| u.is_some()) }; // Compute emotions_available from UUID presence (not path resolution) let emotions_available = [ - has_any(&[avatar.e_neutral_0, avatar.e_neutral_1, avatar.e_neutral_2, avatar.e_neutral_3, avatar.e_neutral_4, avatar.e_neutral_5, avatar.e_neutral_6, avatar.e_neutral_7, avatar.e_neutral_8]), - has_any(&[avatar.e_happy_0, avatar.e_happy_1, avatar.e_happy_2, avatar.e_happy_3, avatar.e_happy_4, avatar.e_happy_5, avatar.e_happy_6, avatar.e_happy_7, avatar.e_happy_8]), - has_any(&[avatar.e_sad_0, avatar.e_sad_1, avatar.e_sad_2, avatar.e_sad_3, avatar.e_sad_4, avatar.e_sad_5, avatar.e_sad_6, avatar.e_sad_7, avatar.e_sad_8]), - has_any(&[avatar.e_angry_0, avatar.e_angry_1, avatar.e_angry_2, avatar.e_angry_3, avatar.e_angry_4, avatar.e_angry_5, avatar.e_angry_6, avatar.e_angry_7, avatar.e_angry_8]), - has_any(&[avatar.e_surprised_0, avatar.e_surprised_1, avatar.e_surprised_2, avatar.e_surprised_3, avatar.e_surprised_4, avatar.e_surprised_5, avatar.e_surprised_6, avatar.e_surprised_7, avatar.e_surprised_8]), - has_any(&[avatar.e_thinking_0, avatar.e_thinking_1, avatar.e_thinking_2, avatar.e_thinking_3, avatar.e_thinking_4, avatar.e_thinking_5, avatar.e_thinking_6, avatar.e_thinking_7, avatar.e_thinking_8]), - has_any(&[avatar.e_laughing_0, avatar.e_laughing_1, avatar.e_laughing_2, avatar.e_laughing_3, avatar.e_laughing_4, avatar.e_laughing_5, avatar.e_laughing_6, avatar.e_laughing_7, avatar.e_laughing_8]), - has_any(&[avatar.e_crying_0, avatar.e_crying_1, avatar.e_crying_2, avatar.e_crying_3, avatar.e_crying_4, avatar.e_crying_5, avatar.e_crying_6, avatar.e_crying_7, avatar.e_crying_8]), - has_any(&[avatar.e_love_0, avatar.e_love_1, avatar.e_love_2, avatar.e_love_3, avatar.e_love_4, avatar.e_love_5, avatar.e_love_6, avatar.e_love_7, avatar.e_love_8]), - has_any(&[avatar.e_confused_0, avatar.e_confused_1, avatar.e_confused_2, avatar.e_confused_3, avatar.e_confused_4, avatar.e_confused_5, avatar.e_confused_6, avatar.e_confused_7, avatar.e_confused_8]), - has_any(&[avatar.e_sleeping_0, avatar.e_sleeping_1, avatar.e_sleeping_2, avatar.e_sleeping_3, avatar.e_sleeping_4, avatar.e_sleeping_5, avatar.e_sleeping_6, avatar.e_sleeping_7, avatar.e_sleeping_8]), - has_any(&[avatar.e_wink_0, avatar.e_wink_1, avatar.e_wink_2, avatar.e_wink_3, avatar.e_wink_4, avatar.e_wink_5, avatar.e_wink_6, avatar.e_wink_7, avatar.e_wink_8]), + has_any(&[ + avatar.e_neutral_0, + avatar.e_neutral_1, + avatar.e_neutral_2, + avatar.e_neutral_3, + avatar.e_neutral_4, + avatar.e_neutral_5, + avatar.e_neutral_6, + avatar.e_neutral_7, + avatar.e_neutral_8, + ]), + has_any(&[ + avatar.e_happy_0, + avatar.e_happy_1, + avatar.e_happy_2, + avatar.e_happy_3, + avatar.e_happy_4, + avatar.e_happy_5, + avatar.e_happy_6, + avatar.e_happy_7, + avatar.e_happy_8, + ]), + has_any(&[ + avatar.e_sad_0, + avatar.e_sad_1, + avatar.e_sad_2, + avatar.e_sad_3, + avatar.e_sad_4, + avatar.e_sad_5, + avatar.e_sad_6, + avatar.e_sad_7, + avatar.e_sad_8, + ]), + has_any(&[ + avatar.e_angry_0, + avatar.e_angry_1, + avatar.e_angry_2, + avatar.e_angry_3, + avatar.e_angry_4, + avatar.e_angry_5, + avatar.e_angry_6, + avatar.e_angry_7, + avatar.e_angry_8, + ]), + has_any(&[ + avatar.e_surprised_0, + avatar.e_surprised_1, + avatar.e_surprised_2, + avatar.e_surprised_3, + avatar.e_surprised_4, + avatar.e_surprised_5, + avatar.e_surprised_6, + avatar.e_surprised_7, + avatar.e_surprised_8, + ]), + has_any(&[ + avatar.e_thinking_0, + avatar.e_thinking_1, + avatar.e_thinking_2, + avatar.e_thinking_3, + avatar.e_thinking_4, + avatar.e_thinking_5, + avatar.e_thinking_6, + avatar.e_thinking_7, + avatar.e_thinking_8, + ]), + has_any(&[ + avatar.e_laughing_0, + avatar.e_laughing_1, + avatar.e_laughing_2, + avatar.e_laughing_3, + avatar.e_laughing_4, + avatar.e_laughing_5, + avatar.e_laughing_6, + avatar.e_laughing_7, + avatar.e_laughing_8, + ]), + has_any(&[ + avatar.e_crying_0, + avatar.e_crying_1, + avatar.e_crying_2, + avatar.e_crying_3, + avatar.e_crying_4, + avatar.e_crying_5, + avatar.e_crying_6, + avatar.e_crying_7, + avatar.e_crying_8, + ]), + has_any(&[ + avatar.e_love_0, + avatar.e_love_1, + avatar.e_love_2, + avatar.e_love_3, + avatar.e_love_4, + avatar.e_love_5, + avatar.e_love_6, + avatar.e_love_7, + avatar.e_love_8, + ]), + has_any(&[ + avatar.e_confused_0, + avatar.e_confused_1, + avatar.e_confused_2, + avatar.e_confused_3, + avatar.e_confused_4, + avatar.e_confused_5, + avatar.e_confused_6, + avatar.e_confused_7, + avatar.e_confused_8, + ]), + has_any(&[ + avatar.e_sleeping_0, + avatar.e_sleeping_1, + avatar.e_sleeping_2, + avatar.e_sleeping_3, + avatar.e_sleeping_4, + avatar.e_sleeping_5, + avatar.e_sleeping_6, + avatar.e_sleeping_7, + avatar.e_sleeping_8, + ]), + has_any(&[ + avatar.e_wink_0, + avatar.e_wink_1, + avatar.e_wink_2, + avatar.e_wink_3, + avatar.e_wink_4, + avatar.e_wink_5, + avatar.e_wink_6, + avatar.e_wink_7, + avatar.e_wink_8, + ]), ]; Ok(Some(AvatarWithPaths { avatar_id: avatar.id, current_emotion: avatar.current_emotion, skin_layer: [ - resolve(avatar.l_skin_0), resolve(avatar.l_skin_1), resolve(avatar.l_skin_2), - resolve(avatar.l_skin_3), resolve(avatar.l_skin_4), resolve(avatar.l_skin_5), - resolve(avatar.l_skin_6), resolve(avatar.l_skin_7), resolve(avatar.l_skin_8), + resolve(avatar.l_skin_0), + resolve(avatar.l_skin_1), + resolve(avatar.l_skin_2), + resolve(avatar.l_skin_3), + resolve(avatar.l_skin_4), + resolve(avatar.l_skin_5), + resolve(avatar.l_skin_6), + resolve(avatar.l_skin_7), + resolve(avatar.l_skin_8), ], clothes_layer: [ - resolve(avatar.l_clothes_0), resolve(avatar.l_clothes_1), resolve(avatar.l_clothes_2), - resolve(avatar.l_clothes_3), resolve(avatar.l_clothes_4), resolve(avatar.l_clothes_5), - resolve(avatar.l_clothes_6), resolve(avatar.l_clothes_7), resolve(avatar.l_clothes_8), + resolve(avatar.l_clothes_0), + resolve(avatar.l_clothes_1), + resolve(avatar.l_clothes_2), + resolve(avatar.l_clothes_3), + resolve(avatar.l_clothes_4), + resolve(avatar.l_clothes_5), + resolve(avatar.l_clothes_6), + resolve(avatar.l_clothes_7), + resolve(avatar.l_clothes_8), ], accessories_layer: [ - resolve(avatar.l_accessories_0), resolve(avatar.l_accessories_1), resolve(avatar.l_accessories_2), - resolve(avatar.l_accessories_3), resolve(avatar.l_accessories_4), resolve(avatar.l_accessories_5), - resolve(avatar.l_accessories_6), resolve(avatar.l_accessories_7), resolve(avatar.l_accessories_8), + resolve(avatar.l_accessories_0), + resolve(avatar.l_accessories_1), + resolve(avatar.l_accessories_2), + resolve(avatar.l_accessories_3), + resolve(avatar.l_accessories_4), + resolve(avatar.l_accessories_5), + resolve(avatar.l_accessories_6), + resolve(avatar.l_accessories_7), + resolve(avatar.l_accessories_8), ], emotions: [ // Neutral (0) [ - resolve(avatar.e_neutral_0), resolve(avatar.e_neutral_1), resolve(avatar.e_neutral_2), - resolve(avatar.e_neutral_3), resolve(avatar.e_neutral_4), resolve(avatar.e_neutral_5), - resolve(avatar.e_neutral_6), resolve(avatar.e_neutral_7), resolve(avatar.e_neutral_8), + resolve(avatar.e_neutral_0), + resolve(avatar.e_neutral_1), + resolve(avatar.e_neutral_2), + resolve(avatar.e_neutral_3), + resolve(avatar.e_neutral_4), + resolve(avatar.e_neutral_5), + resolve(avatar.e_neutral_6), + resolve(avatar.e_neutral_7), + resolve(avatar.e_neutral_8), ], // Happy (1) [ - resolve(avatar.e_happy_0), resolve(avatar.e_happy_1), resolve(avatar.e_happy_2), - resolve(avatar.e_happy_3), resolve(avatar.e_happy_4), resolve(avatar.e_happy_5), - resolve(avatar.e_happy_6), resolve(avatar.e_happy_7), resolve(avatar.e_happy_8), + resolve(avatar.e_happy_0), + resolve(avatar.e_happy_1), + resolve(avatar.e_happy_2), + resolve(avatar.e_happy_3), + resolve(avatar.e_happy_4), + resolve(avatar.e_happy_5), + resolve(avatar.e_happy_6), + resolve(avatar.e_happy_7), + resolve(avatar.e_happy_8), ], // Sad (2) [ - resolve(avatar.e_sad_0), resolve(avatar.e_sad_1), resolve(avatar.e_sad_2), - resolve(avatar.e_sad_3), resolve(avatar.e_sad_4), resolve(avatar.e_sad_5), - resolve(avatar.e_sad_6), resolve(avatar.e_sad_7), resolve(avatar.e_sad_8), + resolve(avatar.e_sad_0), + resolve(avatar.e_sad_1), + resolve(avatar.e_sad_2), + resolve(avatar.e_sad_3), + resolve(avatar.e_sad_4), + resolve(avatar.e_sad_5), + resolve(avatar.e_sad_6), + resolve(avatar.e_sad_7), + resolve(avatar.e_sad_8), ], // Angry (3) [ - resolve(avatar.e_angry_0), resolve(avatar.e_angry_1), resolve(avatar.e_angry_2), - resolve(avatar.e_angry_3), resolve(avatar.e_angry_4), resolve(avatar.e_angry_5), - resolve(avatar.e_angry_6), resolve(avatar.e_angry_7), resolve(avatar.e_angry_8), + resolve(avatar.e_angry_0), + resolve(avatar.e_angry_1), + resolve(avatar.e_angry_2), + resolve(avatar.e_angry_3), + resolve(avatar.e_angry_4), + resolve(avatar.e_angry_5), + resolve(avatar.e_angry_6), + resolve(avatar.e_angry_7), + resolve(avatar.e_angry_8), ], // Surprised (4) [ - resolve(avatar.e_surprised_0), resolve(avatar.e_surprised_1), resolve(avatar.e_surprised_2), - resolve(avatar.e_surprised_3), resolve(avatar.e_surprised_4), resolve(avatar.e_surprised_5), - resolve(avatar.e_surprised_6), resolve(avatar.e_surprised_7), resolve(avatar.e_surprised_8), + resolve(avatar.e_surprised_0), + resolve(avatar.e_surprised_1), + resolve(avatar.e_surprised_2), + resolve(avatar.e_surprised_3), + resolve(avatar.e_surprised_4), + resolve(avatar.e_surprised_5), + resolve(avatar.e_surprised_6), + resolve(avatar.e_surprised_7), + resolve(avatar.e_surprised_8), ], // Thinking (5) [ - resolve(avatar.e_thinking_0), resolve(avatar.e_thinking_1), resolve(avatar.e_thinking_2), - resolve(avatar.e_thinking_3), resolve(avatar.e_thinking_4), resolve(avatar.e_thinking_5), - resolve(avatar.e_thinking_6), resolve(avatar.e_thinking_7), resolve(avatar.e_thinking_8), + resolve(avatar.e_thinking_0), + resolve(avatar.e_thinking_1), + resolve(avatar.e_thinking_2), + resolve(avatar.e_thinking_3), + resolve(avatar.e_thinking_4), + resolve(avatar.e_thinking_5), + resolve(avatar.e_thinking_6), + resolve(avatar.e_thinking_7), + resolve(avatar.e_thinking_8), ], // Laughing (6) [ - resolve(avatar.e_laughing_0), resolve(avatar.e_laughing_1), resolve(avatar.e_laughing_2), - resolve(avatar.e_laughing_3), resolve(avatar.e_laughing_4), resolve(avatar.e_laughing_5), - resolve(avatar.e_laughing_6), resolve(avatar.e_laughing_7), resolve(avatar.e_laughing_8), + resolve(avatar.e_laughing_0), + resolve(avatar.e_laughing_1), + resolve(avatar.e_laughing_2), + resolve(avatar.e_laughing_3), + resolve(avatar.e_laughing_4), + resolve(avatar.e_laughing_5), + resolve(avatar.e_laughing_6), + resolve(avatar.e_laughing_7), + resolve(avatar.e_laughing_8), ], // Crying (7) [ - resolve(avatar.e_crying_0), resolve(avatar.e_crying_1), resolve(avatar.e_crying_2), - resolve(avatar.e_crying_3), resolve(avatar.e_crying_4), resolve(avatar.e_crying_5), - resolve(avatar.e_crying_6), resolve(avatar.e_crying_7), resolve(avatar.e_crying_8), + resolve(avatar.e_crying_0), + resolve(avatar.e_crying_1), + resolve(avatar.e_crying_2), + resolve(avatar.e_crying_3), + resolve(avatar.e_crying_4), + resolve(avatar.e_crying_5), + resolve(avatar.e_crying_6), + resolve(avatar.e_crying_7), + resolve(avatar.e_crying_8), ], // Love (8) [ - resolve(avatar.e_love_0), resolve(avatar.e_love_1), resolve(avatar.e_love_2), - resolve(avatar.e_love_3), resolve(avatar.e_love_4), resolve(avatar.e_love_5), - resolve(avatar.e_love_6), resolve(avatar.e_love_7), resolve(avatar.e_love_8), + resolve(avatar.e_love_0), + resolve(avatar.e_love_1), + resolve(avatar.e_love_2), + resolve(avatar.e_love_3), + resolve(avatar.e_love_4), + resolve(avatar.e_love_5), + resolve(avatar.e_love_6), + resolve(avatar.e_love_7), + resolve(avatar.e_love_8), ], // Confused (9) [ - resolve(avatar.e_confused_0), resolve(avatar.e_confused_1), resolve(avatar.e_confused_2), - resolve(avatar.e_confused_3), resolve(avatar.e_confused_4), resolve(avatar.e_confused_5), - resolve(avatar.e_confused_6), resolve(avatar.e_confused_7), resolve(avatar.e_confused_8), + resolve(avatar.e_confused_0), + resolve(avatar.e_confused_1), + resolve(avatar.e_confused_2), + resolve(avatar.e_confused_3), + resolve(avatar.e_confused_4), + resolve(avatar.e_confused_5), + resolve(avatar.e_confused_6), + resolve(avatar.e_confused_7), + resolve(avatar.e_confused_8), ], // Sleeping (10) [ - resolve(avatar.e_sleeping_0), resolve(avatar.e_sleeping_1), resolve(avatar.e_sleeping_2), - resolve(avatar.e_sleeping_3), resolve(avatar.e_sleeping_4), resolve(avatar.e_sleeping_5), - resolve(avatar.e_sleeping_6), resolve(avatar.e_sleeping_7), resolve(avatar.e_sleeping_8), + resolve(avatar.e_sleeping_0), + resolve(avatar.e_sleeping_1), + resolve(avatar.e_sleeping_2), + resolve(avatar.e_sleeping_3), + resolve(avatar.e_sleeping_4), + resolve(avatar.e_sleeping_5), + resolve(avatar.e_sleeping_6), + resolve(avatar.e_sleeping_7), + resolve(avatar.e_sleeping_8), ], // Wink (11) [ - resolve(avatar.e_wink_0), resolve(avatar.e_wink_1), resolve(avatar.e_wink_2), - resolve(avatar.e_wink_3), resolve(avatar.e_wink_4), resolve(avatar.e_wink_5), - resolve(avatar.e_wink_6), resolve(avatar.e_wink_7), resolve(avatar.e_wink_8), + resolve(avatar.e_wink_0), + resolve(avatar.e_wink_1), + resolve(avatar.e_wink_2), + resolve(avatar.e_wink_3), + resolve(avatar.e_wink_4), + resolve(avatar.e_wink_5), + resolve(avatar.e_wink_6), + resolve(avatar.e_wink_7), + resolve(avatar.e_wink_8), ], ], emotions_available, diff --git a/crates/chattyness-db/src/queries/guests.rs b/crates/chattyness-db/src/queries/guests.rs index 3433781..fcf4274 100644 --- a/crates/chattyness-db/src/queries/guests.rs +++ b/crates/chattyness-db/src/queries/guests.rs @@ -49,7 +49,10 @@ pub async fn create_guest_session( } /// Get a guest session by ID. -pub async fn get_guest_session(pool: &PgPool, session_id: Uuid) -> Result, AppError> { +pub async fn get_guest_session( + pool: &PgPool, + session_id: Uuid, +) -> Result, AppError> { let session = sqlx::query_as::<_, GuestSession>( r#" SELECT id, guest_name, current_realm_id, expires_at, created_at diff --git a/crates/chattyness-db/src/queries/loose_props.rs b/crates/chattyness-db/src/queries/loose_props.rs index abedd94..8819119 100644 --- a/crates/chattyness-db/src/queries/loose_props.rs +++ b/crates/chattyness-db/src/queries/loose_props.rs @@ -57,7 +57,22 @@ pub async fn drop_prop_to_canvas<'e>( ) -> Result { // Single CTE that checks existence/droppability and performs the operation atomically. // Returns status flags plus the LooseProp data (if successful). - let result: Option<(bool, bool, bool, Option, Option, Option, Option, Option, Option, Option, Option>, Option>, Option, Option)> = sqlx::query_as( + let result: Option<( + bool, + bool, + bool, + Option, + Option, + Option, + Option, + Option, + Option, + Option, + Option>, + Option>, + Option, + Option, + )> = sqlx::query_as( r#" WITH item_info AS ( SELECT id, is_droppable, server_prop_id, realm_prop_id, prop_name, prop_asset_path diff --git a/crates/chattyness-db/src/queries/owner/helpers.rs b/crates/chattyness-db/src/queries/owner/helpers.rs index 21193c0..9ae9327 100644 --- a/crates/chattyness-db/src/queries/owner/helpers.rs +++ b/crates/chattyness-db/src/queries/owner/helpers.rs @@ -5,8 +5,8 @@ use chattyness_error::AppError; /// Hash a password using argon2. pub fn hash_password(password: &str) -> Result { use argon2::{ - password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, Argon2, + password_hash::{PasswordHasher, SaltString, rand_core::OsRng}, }; let salt = SaltString::generate(&mut OsRng); diff --git a/crates/chattyness-db/src/queries/props.rs b/crates/chattyness-db/src/queries/props.rs index 82dce8c..e4b7f88 100644 --- a/crates/chattyness-db/src/queries/props.rs +++ b/crates/chattyness-db/src/queries/props.rs @@ -95,25 +95,24 @@ pub async fn create_server_prop<'e>( // Positioning: either content layer OR emotion layer OR neither (all NULL) // Database constraint enforces mutual exclusivity - let (default_layer, default_emotion, default_position) = - if req.default_layer.is_some() { - // Content layer prop - ( - req.default_layer.map(|l| l.to_string()), - None, - Some(req.default_position.unwrap_or(4)), // Default to center position - ) - } else if req.default_emotion.is_some() { - // Emotion layer prop - ( - None, - req.default_emotion.map(|e| e.to_string()), - Some(req.default_position.unwrap_or(4)), // Default to center position - ) - } else { - // Non-avatar prop - (None, None, None) - }; + let (default_layer, default_emotion, default_position) = if req.default_layer.is_some() { + // Content layer prop + ( + req.default_layer.map(|l| l.to_string()), + None, + Some(req.default_position.unwrap_or(4)), // Default to center position + ) + } else if req.default_emotion.is_some() { + // Emotion layer prop + ( + None, + req.default_emotion.map(|e| e.to_string()), + Some(req.default_position.unwrap_or(4)), // Default to center position + ) + } else { + // Non-avatar prop + (None, None, None) + }; let is_droppable = req.droppable.unwrap_or(true); let is_public = req.public.unwrap_or(false); @@ -187,25 +186,24 @@ pub async fn upsert_server_prop<'e>( // Positioning: either content layer OR emotion layer OR neither (all NULL) // Database constraint enforces mutual exclusivity - let (default_layer, default_emotion, default_position) = - if req.default_layer.is_some() { - // Content layer prop - ( - req.default_layer.map(|l| l.to_string()), - None, - Some(req.default_position.unwrap_or(4)), // Default to center position - ) - } else if req.default_emotion.is_some() { - // Emotion layer prop - ( - None, - req.default_emotion.map(|e| e.to_string()), - Some(req.default_position.unwrap_or(4)), // Default to center position - ) - } else { - // Non-avatar prop - (None, None, None) - }; + let (default_layer, default_emotion, default_position) = if req.default_layer.is_some() { + // Content layer prop + ( + req.default_layer.map(|l| l.to_string()), + None, + Some(req.default_position.unwrap_or(4)), // Default to center position + ) + } else if req.default_emotion.is_some() { + // Emotion layer prop + ( + None, + req.default_emotion.map(|e| e.to_string()), + Some(req.default_position.unwrap_or(4)), // Default to center position + ) + } else { + // Non-avatar prop + (None, None, None) + }; let is_droppable = req.droppable.unwrap_or(true); let is_public = req.public.unwrap_or(false); diff --git a/crates/chattyness-db/src/queries/realms.rs b/crates/chattyness-db/src/queries/realms.rs index ef5f555..2bc7482 100644 --- a/crates/chattyness-db/src/queries/realms.rs +++ b/crates/chattyness-db/src/queries/realms.rs @@ -60,12 +60,11 @@ pub async fn create_realm( /// Check if a realm slug is available. pub async fn is_slug_available(pool: &PgPool, slug: &str) -> Result { - let exists: (bool,) = sqlx::query_as( - r#"SELECT EXISTS(SELECT 1 FROM realm.realms WHERE slug = $1)"#, - ) - .bind(slug) - .fetch_one(pool) - .await?; + let exists: (bool,) = + sqlx::query_as(r#"SELECT EXISTS(SELECT 1 FROM realm.realms WHERE slug = $1)"#) + .bind(slug) + .fetch_one(pool) + .await?; Ok(!exists.0) } diff --git a/crates/chattyness-db/src/queries/scenes.rs b/crates/chattyness-db/src/queries/scenes.rs index f5609d0..5d6405e 100644 --- a/crates/chattyness-db/src/queries/scenes.rs +++ b/crates/chattyness-db/src/queries/scenes.rs @@ -116,12 +116,13 @@ pub async fn is_scene_slug_available<'e>( realm_id: Uuid, slug: &str, ) -> Result { - let exists: (bool,) = - sqlx::query_as(r#"SELECT EXISTS(SELECT 1 FROM realm.scenes WHERE realm_id = $1 AND slug = $2)"#) - .bind(realm_id) - .bind(slug) - .fetch_one(executor) - .await?; + let exists: (bool,) = sqlx::query_as( + r#"SELECT EXISTS(SELECT 1 FROM realm.scenes WHERE realm_id = $1 AND slug = $2)"#, + ) + .bind(realm_id) + .bind(slug) + .fetch_one(executor) + .await?; Ok(!exists.0) } @@ -294,7 +295,10 @@ pub async fn update_scene<'e>( param_idx += 1; } if req.dimension_mode.is_some() { - set_clauses.push(format!("dimension_mode = ${}::realm.dimension_mode", param_idx)); + set_clauses.push(format!( + "dimension_mode = ${}::realm.dimension_mode", + param_idx + )); param_idx += 1; } if req.sort_order.is_some() { @@ -317,7 +321,8 @@ pub async fn update_scene<'e>( s.is_hidden, s.created_at, s.updated_at, c.id as default_channel_id FROM realm.scenes s LEFT JOIN scene.instances c ON c.scene_id = s.id AND c.instance_type = 'public' - WHERE s.id = $1"#.to_string() + WHERE s.id = $1"# + .to_string() } else { set_clauses.push("updated_at = now()".to_string()); format!( @@ -396,12 +401,11 @@ pub async fn get_next_sort_order<'e>( executor: impl PgExecutor<'e>, realm_id: Uuid, ) -> Result { - let result: (Option,) = sqlx::query_as( - r#"SELECT MAX(sort_order) FROM realm.scenes WHERE realm_id = $1"#, - ) - .bind(realm_id) - .fetch_one(executor) - .await?; + let result: (Option,) = + sqlx::query_as(r#"SELECT MAX(sort_order) FROM realm.scenes WHERE realm_id = $1"#) + .bind(realm_id) + .fetch_one(executor) + .await?; Ok(result.0.unwrap_or(0) + 1) } diff --git a/crates/chattyness-db/src/queries/spots.rs b/crates/chattyness-db/src/queries/spots.rs index d024072..7160c9f 100644 --- a/crates/chattyness-db/src/queries/spots.rs +++ b/crates/chattyness-db/src/queries/spots.rs @@ -108,12 +108,13 @@ pub async fn is_spot_slug_available<'e>( scene_id: Uuid, slug: &str, ) -> Result { - let exists: (bool,) = - sqlx::query_as(r#"SELECT EXISTS(SELECT 1 FROM scene.spots WHERE scene_id = $1 AND slug = $2)"#) - .bind(scene_id) - .bind(slug) - .fetch_one(executor) - .await?; + let exists: (bool,) = sqlx::query_as( + r#"SELECT EXISTS(SELECT 1 FROM scene.spots WHERE scene_id = $1 AND slug = $2)"#, + ) + .bind(scene_id) + .bind(slug) + .fetch_one(executor) + .await?; Ok(!exists.0) } @@ -236,7 +237,8 @@ pub async fn update_spot<'e>( ST_AsText(destination_position) as destination_position_wkt, current_state, sort_order, is_visible, is_active, created_at, updated_at - FROM scene.spots WHERE id = $1"#.to_string() + FROM scene.spots WHERE id = $1"# + .to_string() } else { set_clauses.push("updated_at = now()".to_string()); format!( @@ -293,10 +295,7 @@ pub async fn update_spot<'e>( } /// Delete a spot. -pub async fn delete_spot<'e>( - executor: impl PgExecutor<'e>, - spot_id: Uuid, -) -> Result<(), AppError> { +pub async fn delete_spot<'e>(executor: impl PgExecutor<'e>, spot_id: Uuid) -> Result<(), AppError> { let result = sqlx::query(r#"DELETE FROM scene.spots WHERE id = $1"#) .bind(spot_id) .execute(executor) diff --git a/crates/chattyness-db/src/queries/users.rs b/crates/chattyness-db/src/queries/users.rs index 29f57e1..609d4ee 100644 --- a/crates/chattyness-db/src/queries/users.rs +++ b/crates/chattyness-db/src/queries/users.rs @@ -293,8 +293,8 @@ pub async fn update_password( new_password: &str, ) -> Result<(), AppError> { use argon2::{ - password_hash::{rand_core::OsRng, SaltString}, Argon2, PasswordHasher, + password_hash::{SaltString, rand_core::OsRng}, }; let salt = SaltString::generate(&mut OsRng); @@ -326,8 +326,8 @@ pub async fn update_password_conn( new_password: &str, ) -> Result<(), AppError> { use argon2::{ - password_hash::{rand_core::OsRng, SaltString}, Argon2, PasswordHasher, + password_hash::{SaltString, rand_core::OsRng}, }; let salt = SaltString::generate(&mut OsRng); @@ -405,8 +405,8 @@ pub async fn create_user( password: &str, ) -> Result { use argon2::{ - password_hash::{rand_core::OsRng, SaltString}, Argon2, PasswordHasher, + password_hash::{SaltString, rand_core::OsRng}, }; let salt = SaltString::generate(&mut OsRng); @@ -442,8 +442,8 @@ pub async fn create_user_conn( password: &str, ) -> Result { use argon2::{ - password_hash::{rand_core::OsRng, SaltString}, Argon2, PasswordHasher, + password_hash::{SaltString, rand_core::OsRng}, }; let salt = SaltString::generate(&mut OsRng); @@ -473,7 +473,10 @@ pub async fn create_user_conn( /// Get a staff member by their user ID. /// /// Returns the staff member with their user info joined. -pub async fn get_staff_member(pool: &PgPool, user_id: Uuid) -> Result, AppError> { +pub async fn get_staff_member( + pool: &PgPool, + user_id: Uuid, +) -> Result, AppError> { let staff = sqlx::query_as::<_, StaffMember>( r#" SELECT diff --git a/crates/chattyness-error/src/lib.rs b/crates/chattyness-error/src/lib.rs index a166b71..fb6e4ff 100644 --- a/crates/chattyness-error/src/lib.rs +++ b/crates/chattyness-error/src/lib.rs @@ -79,9 +79,9 @@ impl From for ErrorResponse { #[cfg(feature = "ssr")] mod ssr_impl { use super::*; + use axum::Json; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; - use axum::Json; impl IntoResponse for AppError { fn into_response(self) -> Response { diff --git a/crates/chattyness-shared/src/validation.rs b/crates/chattyness-shared/src/validation.rs index 0fb31d7..c79ac30 100644 --- a/crates/chattyness-shared/src/validation.rs +++ b/crates/chattyness-shared/src/validation.rs @@ -160,7 +160,12 @@ pub fn validate_non_empty(value: &str, field_name: &str) -> Result<(), AppError> /// # Returns /// - `Ok(())` if length is within bounds /// - `Err(AppError::Validation)` if too short or too long -pub fn validate_length(value: &str, field_name: &str, min: usize, max: usize) -> Result<(), AppError> { +pub fn validate_length( + value: &str, + field_name: &str, + min: usize, + max: usize, +) -> Result<(), AppError> { let len = value.len(); if len < min || len > max { return Err(AppError::Validation(format!( diff --git a/crates/chattyness-user-ui/src/api/auth.rs b/crates/chattyness-user-ui/src/api/auth.rs index a50bb7f..689fd6a 100644 --- a/crates/chattyness-user-ui/src/api/auth.rs +++ b/crates/chattyness-user-ui/src/api/auth.rs @@ -1,9 +1,6 @@ //! Authentication API handlers. -use axum::{ - extract::State, - Json, -}; +use axum::{Json, extract::State}; use sqlx::PgPool; use tower_sessions::Session; @@ -19,11 +16,11 @@ use chattyness_db::{ use chattyness_error::AppError; use crate::auth::{ + AuthUser, OptionalAuthUser, session::{ SESSION_CURRENT_REALM_KEY, SESSION_LOGIN_TYPE_KEY, SESSION_ORIGINAL_DEST_KEY, SESSION_USER_ID_KEY, }, - AuthUser, OptionalAuthUser, }; /// Get current user info. @@ -66,7 +63,9 @@ pub async fn login( .ok_or(AppError::InvalidCredentials)?; // Set RLS context to the authenticated user for subsequent operations - rls_conn.set_user_id(user.id).await + rls_conn + .set_user_id(user.id) + .await .map_err(|e| AppError::Internal(format!("Failed to set RLS context: {}", e)))?; // Check account status @@ -285,19 +284,27 @@ pub async fn signup( }); let mut conn = rls_conn.acquire().await; - let user_id = - users::create_user_conn(&mut *conn, &req.username, email_opt, req.display_name.trim(), &req.password) - .await?; + let user_id = users::create_user_conn( + &mut *conn, + &req.username, + email_opt, + req.display_name.trim(), + &req.password, + ) + .await?; drop(conn); // Set RLS context to the new user for membership creation - rls_conn.set_user_id(user_id).await + rls_conn + .set_user_id(user_id) + .await .map_err(|e| AppError::Internal(format!("Failed to set RLS context: {}", e)))?; // Create membership using RLS connection (now has user context) let mut conn = rls_conn.acquire().await; let membership_id = - memberships::create_membership_conn(&mut *conn, user_id, realm.id, RealmRole::Member).await?; + memberships::create_membership_conn(&mut *conn, user_id, realm.id, RealmRole::Member) + .await?; // Set up session (user is logged in) session @@ -421,7 +428,8 @@ pub async fn join_realm( // Create the membership using RLS connection (policy requires user_id = current_user_id) let mut conn = rls_conn.acquire().await; let membership_id = - memberships::create_membership_conn(&mut *conn, user.id, realm.id, RealmRole::Member).await?; + memberships::create_membership_conn(&mut *conn, user.id, realm.id, RealmRole::Member) + .await?; Ok(Json(JoinRealmResponse { success: true, diff --git a/crates/chattyness-user-ui/src/api/avatars.rs b/crates/chattyness-user-ui/src/api/avatars.rs index 514f5bd..c3c344d 100644 --- a/crates/chattyness-user-ui/src/api/avatars.rs +++ b/crates/chattyness-user-ui/src/api/avatars.rs @@ -3,8 +3,8 @@ //! Handles avatar data retrieval and slot updates. //! Note: Emotion switching is handled via WebSocket. -use axum::extract::Path; use axum::Json; +use axum::extract::Path; use chattyness_db::{ models::{AssignSlotRequest, AvatarWithPaths, ClearSlotRequest}, diff --git a/crates/chattyness-user-ui/src/api/inventory.rs b/crates/chattyness-user-ui/src/api/inventory.rs index dc82ce0..7fd643c 100644 --- a/crates/chattyness-user-ui/src/api/inventory.rs +++ b/crates/chattyness-user-ui/src/api/inventory.rs @@ -2,8 +2,8 @@ //! //! Handles inventory listing and item management. -use axum::extract::{Path, State}; use axum::Json; +use axum::extract::{Path, State}; use sqlx::PgPool; use uuid::Uuid; diff --git a/crates/chattyness-user-ui/src/api/realms.rs b/crates/chattyness-user-ui/src/api/realms.rs index 0acbcf1..98522f4 100644 --- a/crates/chattyness-user-ui/src/api/realms.rs +++ b/crates/chattyness-user-ui/src/api/realms.rs @@ -1,8 +1,8 @@ //! Realm API handlers for user UI (READ-ONLY). use axum::{ - extract::{Path, Query, State}, Json, + extract::{Path, Query, State}, }; use serde::{Deserialize, Serialize}; use sqlx::PgPool; diff --git a/crates/chattyness-user-ui/src/api/routes.rs b/crates/chattyness-user-ui/src/api/routes.rs index 22d4142..273df49 100644 --- a/crates/chattyness-user-ui/src/api/routes.rs +++ b/crates/chattyness-user-ui/src/api/routes.rs @@ -4,7 +4,7 @@ //! All create/update/delete operations are handled by the admin-ui. //! Channel presence is handled via WebSocket. -use axum::{routing::get, Router}; +use axum::{Router, routing::get}; use super::{auth, avatars, inventory, realms, scenes, websocket}; use crate::app::AppState; @@ -30,7 +30,10 @@ pub fn api_router() -> Router { // Realm routes (READ-ONLY) .route("/realms", get(realms::list_realms)) .route("/realms/{slug}", get(realms::get_realm)) - .route("/realms/{slug}/available", get(realms::check_slug_available)) + .route( + "/realms/{slug}/available", + get(realms::check_slug_available), + ) // Scene routes (READ-ONLY) .route("/realms/{slug}/entry-scene", get(scenes::get_entry_scene)) .route("/realms/{slug}/scenes", get(scenes::list_scenes)) @@ -63,8 +66,5 @@ pub fn api_router() -> Router { ) // Public inventory routes (public server/realm props) .route("/inventory/server", get(inventory::get_server_props)) - .route( - "/realms/{slug}/inventory", - get(inventory::get_realm_props), - ) + .route("/realms/{slug}/inventory", get(inventory::get_realm_props)) } diff --git a/crates/chattyness-user-ui/src/api/scenes.rs b/crates/chattyness-user-ui/src/api/scenes.rs index 9e2586c..d83f99a 100644 --- a/crates/chattyness-user-ui/src/api/scenes.rs +++ b/crates/chattyness-user-ui/src/api/scenes.rs @@ -1,8 +1,8 @@ //! Scene and Spot API handlers for user UI (READ-ONLY). use axum::{ - extract::{Path, State}, Json, + extract::{Path, State}, }; use sqlx::PgPool; use uuid::Uuid; diff --git a/crates/chattyness-user-ui/src/api/websocket.rs b/crates/chattyness-user-ui/src/api/websocket.rs index 0f7d558..fa958dc 100644 --- a/crates/chattyness-user-ui/src/api/websocket.rs +++ b/crates/chattyness-user-ui/src/api/websocket.rs @@ -4,8 +4,8 @@ use axum::{ extract::{ - ws::{CloseFrame, Message, WebSocket, WebSocketUpgrade}, FromRef, Path, State, + ws::{CloseFrame, Message, WebSocket, WebSocketUpgrade}, }, response::IntoResponse, }; @@ -109,7 +109,11 @@ impl WebSocketState { } /// Find a user by display name within a realm. - pub fn find_user_by_display_name(&self, realm_id: Uuid, display_name: &str) -> Option<(Uuid, UserConnection)> { + pub fn find_user_by_display_name( + &self, + realm_id: Uuid, + display_name: &str, + ) -> Option<(Uuid, UserConnection)> { for entry in self.users.iter() { let (user_id, conn) = entry.pair(); if conn.realm_id == realm_id && conn.display_name.eq_ignore_ascii_case(display_name) { @@ -148,11 +152,19 @@ where "[WS] Connection attempt to {}/channels/{} - auth: {:?}", slug, channel_id, - auth_result.as_ref().map(|a| a.0.id).map_err(|e| format!("{:?}", e)) + auth_result + .as_ref() + .map(|a| a.0.id) + .map_err(|e| format!("{:?}", e)) ); let AuthUser(user) = auth_result.map_err(|e| { - tracing::warn!("[WS] Auth failed for {}/channels/{}: {:?}", slug, channel_id, e); + tracing::warn!( + "[WS] Auth failed for {}/channels/{}: {:?}", + slug, + channel_id, + e + ); AppError::from(e) })?; @@ -181,7 +193,9 @@ where ); Ok(ws.on_upgrade(move |socket| { - handle_socket(socket, user, channel_id, realm.id, pool, ws_state, ws_config) + handle_socket( + socket, user, channel_id, realm.id, pool, ws_state, ws_config, + ) })) } @@ -225,7 +239,11 @@ async fn handle_socket( // Set RLS context on this dedicated connection if let Err(e) = set_rls_user_id(&mut conn, user.id).await { - tracing::error!("[WS] Failed to set RLS context for user {}: {:?}", user.id, e); + tracing::error!( + "[WS] Failed to set RLS context for user {}: {:?}", + user.id, + e + ); return; } tracing::info!("[WS] RLS context set on dedicated connection"); @@ -266,8 +284,10 @@ async fn handle_socket( } }; - let member = match channel_members::get_channel_member(&mut *conn, channel_id, user.id, realm_id) - .await + let member = match channel_members::get_channel_member( + &mut *conn, channel_id, user.id, realm_id, + ) + .await { Ok(Some(m)) => m, Ok(None) => { @@ -389,15 +409,21 @@ async fn handle_socket( #[cfg(debug_assertions)] tracing::debug!("[WS<-Client] {}", text); - let Ok(client_msg) = serde_json::from_str::(&text) else { + let Ok(client_msg) = serde_json::from_str::(&text) + else { continue; }; match client_msg { ClientMessage::UpdatePosition { x, y } => { - if let Err(e) = - channel_members::update_position(&mut *recv_conn, channel_id, user_id, x, y) - .await + if let Err(e) = channel_members::update_position( + &mut *recv_conn, + channel_id, + user_id, + x, + y, + ) + .await { #[cfg(debug_assertions)] tracing::error!("[WS] Position update failed: {:?}", e); @@ -416,7 +442,10 @@ async fn handle_socket( Ok(e) => e, Err(_) => { #[cfg(debug_assertions)] - tracing::warn!("[WS] Invalid emotion name: {}", emotion); + tracing::warn!( + "[WS] Invalid emotion name: {}", + emotion + ); continue; } }; @@ -444,11 +473,19 @@ async fn handle_socket( } ClientMessage::Ping => { // Update last_moved_at to keep member alive for cleanup - let _ = channel_members::touch_member(&mut *recv_conn, channel_id, user_id).await; + let _ = channel_members::touch_member( + &mut *recv_conn, + channel_id, + user_id, + ) + .await; // Respond with pong directly (not broadcast) let _ = direct_tx.send(ServerMessage::Pong).await; } - ClientMessage::SendChatMessage { content, target_display_name } => { + ClientMessage::SendChatMessage { + content, + target_display_name, + } => { // Validate message if content.is_empty() || content.len() > 500 { continue; @@ -465,18 +502,20 @@ async fn handle_socket( if let Ok(Some(member)) = member_info { // Convert emotion index to name - let emotion_name = EmotionState::from_index(member.current_emotion as u8) - .map(|e| e.to_string()) - .unwrap_or_else(|| "neutral".to_string()); + let emotion_name = + EmotionState::from_index(member.current_emotion as u8) + .map(|e| e.to_string()) + .unwrap_or_else(|| "neutral".to_string()); // Handle whisper (direct message) vs broadcast if let Some(target_name) = target_display_name { // Whisper: send directly to target user - if let Some((_target_user_id, target_conn)) = - ws_state.find_user_by_display_name(realm_id, &target_name) + if let Some((_target_user_id, target_conn)) = ws_state + .find_user_by_display_name(realm_id, &target_name) { // Determine if same scene - let is_same_scene = target_conn.channel_id == channel_id; + let is_same_scene = + target_conn.channel_id == channel_id; let msg = ServerMessage::ChatMessageReceived { message_id: Uuid::new_v4(), @@ -487,29 +526,33 @@ async fn handle_socket( emotion: emotion_name.clone(), x: member.position_x, y: member.position_y, - timestamp: chrono::Utc::now().timestamp_millis(), + timestamp: chrono::Utc::now() + .timestamp_millis(), is_whisper: true, is_same_scene, }; // Send to target user - let _ = target_conn.direct_tx.send(msg.clone()).await; + let _ = + target_conn.direct_tx.send(msg.clone()).await; // Also send back to sender (so they see their own whisper) // For sender, is_same_scene is always true (they see it as a bubble) - let sender_msg = ServerMessage::ChatMessageReceived { - message_id: Uuid::new_v4(), - user_id: Some(user_id), - guest_session_id: None, - display_name: member.display_name.clone(), - content, - emotion: emotion_name, - x: member.position_x, - y: member.position_y, - timestamp: chrono::Utc::now().timestamp_millis(), - is_whisper: true, - is_same_scene: true, // Sender always sees as bubble - }; + let sender_msg = + ServerMessage::ChatMessageReceived { + message_id: Uuid::new_v4(), + user_id: Some(user_id), + guest_session_id: None, + display_name: member.display_name.clone(), + content, + emotion: emotion_name, + x: member.position_x, + y: member.position_y, + timestamp: chrono::Utc::now() + .timestamp_millis(), + is_whisper: true, + is_same_scene: true, // Sender always sees as bubble + }; let _ = direct_tx.send(sender_msg).await; #[cfg(debug_assertions)] @@ -581,20 +624,26 @@ async fn handle_socket( pos_x, pos_y ); - let _ = tx.send(ServerMessage::PropDropped { prop }); + let _ = + tx.send(ServerMessage::PropDropped { prop }); } Err(e) => { tracing::error!("[WS] Drop prop failed: {:?}", e); let (code, message) = match &e { - chattyness_error::AppError::Forbidden(msg) => { - ("PROP_NOT_DROPPABLE".to_string(), msg.clone()) - } + chattyness_error::AppError::Forbidden(msg) => ( + "PROP_NOT_DROPPABLE".to_string(), + msg.clone(), + ), chattyness_error::AppError::NotFound(msg) => { ("PROP_NOT_FOUND".to_string(), msg.clone()) } - _ => ("DROP_FAILED".to_string(), format!("{:?}", e)), + _ => ( + "DROP_FAILED".to_string(), + format!("{:?}", e), + ), }; - let _ = tx.send(ServerMessage::Error { code, message }); + let _ = + tx.send(ServerMessage::Error { code, message }); } } } @@ -653,7 +702,10 @@ async fn handle_socket( } Ok(None) => { #[cfg(debug_assertions)] - tracing::warn!("[WS] No avatar found for user {} to sync", user_id); + tracing::warn!( + "[WS] No avatar found for user {} to sync", + user_id + ); } Err(e) => { tracing::error!("[WS] Avatar sync failed: {:?}", e); diff --git a/crates/chattyness-user-ui/src/app.rs b/crates/chattyness-user-ui/src/app.rs index 74bdb41..a6731d1 100644 --- a/crates/chattyness-user-ui/src/app.rs +++ b/crates/chattyness-user-ui/src/app.rs @@ -1,7 +1,7 @@ //! Leptos application root and router for public app. use leptos::prelude::*; -use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title}; +use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context}; use leptos_router::components::Router; use crate::routes::UserRoutes; diff --git a/crates/chattyness-user-ui/src/auth/middleware.rs b/crates/chattyness-user-ui/src/auth/middleware.rs index d993d17..d9b62b5 100644 --- a/crates/chattyness-user-ui/src/auth/middleware.rs +++ b/crates/chattyness-user-ui/src/auth/middleware.rs @@ -1,10 +1,10 @@ //! Authentication middleware and extractors. use axum::{ - extract::{FromRef, FromRequestParts}, - http::{request::Parts, StatusCode}, - response::{IntoResponse, Response}, Json, + extract::{FromRef, FromRequestParts}, + http::{StatusCode, request::Parts}, + response::{IntoResponse, Response}, }; use sqlx::PgPool; use tower_sessions::Session; diff --git a/crates/chattyness-user-ui/src/auth/rls.rs b/crates/chattyness-user-ui/src/auth/rls.rs index 00d6dc9..5c307f8 100644 --- a/crates/chattyness-user-ui/src/auth/rls.rs +++ b/crates/chattyness-user-ui/src/auth/rls.rs @@ -1,12 +1,12 @@ //! Row-Level Security (RLS) middleware for PostgreSQL. use axum::{ - extract::FromRequestParts, - http::{request::Parts, Request, StatusCode}, - response::{IntoResponse, Response}, Json, + extract::FromRequestParts, + http::{Request, StatusCode, request::Parts}, + response::{IntoResponse, Response}, }; -use sqlx::{pool::PoolConnection, postgres::PgConnection, PgPool, Postgres}; +use sqlx::{PgPool, Postgres, pool::PoolConnection, postgres::PgConnection}; use std::{ future::Future, ops::{Deref, DerefMut}, @@ -148,9 +148,10 @@ impl IntoResponse for RlsError { StatusCode::INTERNAL_SERVER_ERROR, "RLS connection not available", ), - RlsError::DatabaseError(msg) => { - (StatusCode::INTERNAL_SERVER_ERROR, msg.leak() as &'static str) - } + RlsError::DatabaseError(msg) => ( + StatusCode::INTERNAL_SERVER_ERROR, + msg.leak() as &'static str, + ), }; let body = ErrorResponse { diff --git a/crates/chattyness-user-ui/src/auth/session.rs b/crates/chattyness-user-ui/src/auth/session.rs index 90b6caf..3b6bfac 100644 --- a/crates/chattyness-user-ui/src/auth/session.rs +++ b/crates/chattyness-user-ui/src/auth/session.rs @@ -1,7 +1,7 @@ //! Session management using tower-sessions. use sqlx::PgPool; -use tower_sessions::{cookie::time::Duration, cookie::SameSite, Expiry, SessionManagerLayer}; +use tower_sessions::{Expiry, SessionManagerLayer, cookie::SameSite, cookie::time::Duration}; use tower_sessions_sqlx_store::PostgresStore; /// Session cookie name. diff --git a/crates/chattyness-user-ui/src/components/avatar_canvas.rs b/crates/chattyness-user-ui/src/components/avatar_canvas.rs index 71772f9..3943066 100644 --- a/crates/chattyness-user-ui/src/components/avatar_canvas.rs +++ b/crates/chattyness-user-ui/src/components/avatar_canvas.rs @@ -9,7 +9,7 @@ use uuid::Uuid; use chattyness_db::models::ChannelMemberWithAvatar; -use super::chat_types::{emotion_bubble_colors, ActiveBubble}; +use super::chat_types::{ActiveBubble, emotion_bubble_colors}; /// Base text size multiplier. Text at 100% slider = base_sizes * 1.4 const BASE_TEXT_SCALE: f64 = 1.4; @@ -65,18 +65,47 @@ impl ContentBounds { let mid_col = [1, 4, 7].iter().any(|&p| has_content_at(p)); let right_col = [2, 5, 8].iter().any(|&p| has_content_at(p)); - let min_col = if left_col { 0 } else if mid_col { 1 } else { 2 }; - let max_col = if right_col { 2 } else if mid_col { 1 } else { 0 }; + let min_col = if left_col { + 0 + } else if mid_col { + 1 + } else { + 2 + }; + let max_col = if right_col { + 2 + } else if mid_col { + 1 + } else { + 0 + }; // Rows: 0 (top), 1 (middle), 2 (bottom) let top_row = [0, 1, 2].iter().any(|&p| has_content_at(p)); let mid_row = [3, 4, 5].iter().any(|&p| has_content_at(p)); let bot_row = [6, 7, 8].iter().any(|&p| has_content_at(p)); - let min_row = if top_row { 0 } else if mid_row { 1 } else { 2 }; - let max_row = if bot_row { 2 } else if mid_row { 1 } else { 0 }; + let min_row = if top_row { + 0 + } else if mid_row { + 1 + } else { + 2 + }; + let max_row = if bot_row { + 2 + } else if mid_row { + 1 + } else { + 0 + }; - Self { min_col, max_col, min_row, max_row } + Self { + min_col, + max_col, + min_row, + max_row, + } } /// Content center column (0.0 to 2.0, grid center is 1.0). @@ -158,8 +187,12 @@ impl ScreenBoundaries { half_width: f64, half_height: f64, ) -> (f64, f64) { - let clamped_x = center_x.max(self.min_x + half_width).min(self.max_x - half_width); - let clamped_y = center_y.max(self.min_y + half_height).min(self.max_y - half_height); + let clamped_x = center_x + .max(self.min_x + half_width) + .min(self.max_x - half_width); + let clamped_y = center_y + .max(self.min_y + half_height) + .min(self.max_y - half_height); (clamped_x, clamped_y) } } @@ -310,7 +343,8 @@ pub fn AvatarCanvas( let avatar_half_height = avatar_size / 2.0 + y_content_offset; // Calculate bubble height using actual content (includes tail + gap) - let estimated_bubble_height = bubble.as_ref() + let estimated_bubble_height = bubble + .as_ref() .map(|b| estimate_bubble_height(&b.message.content, text_scale)) .unwrap_or(0.0); @@ -363,8 +397,8 @@ pub fn AvatarCanvas( use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; - use wasm_bindgen::closure::Closure; use wasm_bindgen::JsCast; + use wasm_bindgen::closure::Closure; // Image cache for this avatar (persists across re-renders) let image_cache: Rc>> = @@ -426,7 +460,8 @@ pub fn AvatarCanvas( let avatar_half_height = avatar_size / 2.0 + y_content_offset; // Calculate bubble height using actual content (includes tail + gap) - let estimated_bubble_height = bubble.as_ref() + let estimated_bubble_height = bubble + .as_ref() .map(|b| estimate_bubble_height(&b.message.content, text_scale)) .unwrap_or(0.0); @@ -470,7 +505,12 @@ pub fn AvatarCanvas( // Helper to load and draw an image // Images are cached; when loaded, triggers a redraw via signal - let draw_image = |path: &str, cache: &Rc>>, ctx: &web_sys::CanvasRenderingContext2d, x: f64, y: f64, size: f64| { + let draw_image = |path: &str, + cache: &Rc>>, + ctx: &web_sys::CanvasRenderingContext2d, + x: f64, + y: f64, + size: f64| { let normalized_path = normalize_asset_path(path); let mut cache_borrow = cache.borrow_mut(); @@ -526,7 +566,14 @@ pub fn AvatarCanvas( let row = pos / 3; let cell_cx = grid_origin_x + (col as f64 + 0.5) * cell_size; let cell_cy = grid_origin_y + (row as f64 + 0.5) * cell_size; - draw_image(clothes_path, &image_cache, &ctx, cell_cx, cell_cy, cell_size); + draw_image( + clothes_path, + &image_cache, + &ctx, + cell_cx, + cell_cy, + cell_size, + ); } } @@ -537,7 +584,14 @@ pub fn AvatarCanvas( let row = pos / 3; let cell_cx = grid_origin_x + (col as f64 + 0.5) * cell_size; let cell_cy = grid_origin_y + (row as f64 + 0.5) * cell_size; - draw_image(accessories_path, &image_cache, &ctx, cell_cx, cell_cy, cell_size); + draw_image( + accessories_path, + &image_cache, + &ctx, + cell_cx, + cell_cy, + cell_size, + ); } } @@ -548,7 +602,14 @@ pub fn AvatarCanvas( let row = pos / 3; let cell_cx = grid_origin_x + (col as f64 + 0.5) * cell_size; let cell_cy = grid_origin_y + (row as f64 + 0.5) * cell_size; - draw_image(emotion_path, &image_cache, &ctx, cell_cx, cell_cy, cell_size); + draw_image( + emotion_path, + &image_cache, + &ctx, + cell_cx, + cell_cy, + cell_size, + ); } } @@ -560,7 +621,13 @@ pub fn AvatarCanvas( let badge_y = avatar_cy - avatar_size / 2.0 - badge_size / 2.0; ctx.begin_path(); - let _ = ctx.arc(badge_x, badge_y, badge_size / 2.0, 0.0, std::f64::consts::PI * 2.0); + let _ = ctx.arc( + badge_x, + badge_y, + badge_size / 2.0, + 0.0, + std::f64::consts::PI * 2.0, + ); ctx.set_fill_style_str("#f59e0b"); ctx.fill(); @@ -580,7 +647,8 @@ pub fn AvatarCanvas( ctx.set_font(&format!("{}px sans-serif", 12.0 * text_scale)); ctx.set_text_align("center"); ctx.set_text_baseline("alphabetic"); - let name_y = avatar_cy + avatar_size / 2.0 - (empty_bottom_rows as f64 * cell_size) + 15.0 * text_scale; + let name_y = avatar_cy + avatar_size / 2.0 - (empty_bottom_rows as f64 * cell_size) + + 15.0 * text_scale; // Black outline ctx.set_stroke_style_str("#000"); ctx.set_line_width(3.0); @@ -630,7 +698,9 @@ pub fn AvatarCanvas( // Compute data-member-id reactively let data_member_id = move || { let m = member.get(); - m.member.user_id.map(|u| u.to_string()) + m.member + .user_id + .map(|u| u.to_string()) .or_else(|| m.member.guest_session_id.map(|g| g.to_string())) .unwrap_or_default() }; @@ -693,11 +763,19 @@ fn draw_bubble( let (bg_color, border_color, text_color) = emotion_bubble_colors(&bubble.message.emotion); // Use italic font for whispers - let font_style = if bubble.message.is_whisper { "italic " } else { "" }; + let font_style = if bubble.message.is_whisper { + "italic " + } else { + "" + }; // Measure and wrap text ctx.set_font(&format!("{}{}px sans-serif", font_style, font_size)); - let lines = wrap_text(ctx, &bubble.message.content, max_bubble_width - padding * 2.0); + let lines = wrap_text( + ctx, + &bubble.message.content, + max_bubble_width - padding * 2.0, + ); // Calculate bubble dimensions let bubble_width = lines @@ -747,7 +825,14 @@ fn draw_bubble( }; // Draw bubble background - draw_rounded_rect(ctx, bubble_x, bubble_y, bubble_width, bubble_height, border_radius); + draw_rounded_rect( + ctx, + bubble_x, + bubble_y, + bubble_width, + bubble_height, + border_radius, + ); ctx.set_fill_style_str(bg_color); ctx.fill(); ctx.set_stroke_style_str(border_color); @@ -782,7 +867,11 @@ fn draw_bubble( ctx.set_text_align("left"); ctx.set_text_baseline("top"); for (i, line) in lines.iter().enumerate() { - let _ = ctx.fill_text(line, bubble_x + padding, bubble_y + padding + (i as f64) * line_height); + let _ = ctx.fill_text( + line, + bubble_x + padding, + bubble_y + padding + (i as f64) * line_height, + ); } } @@ -800,7 +889,10 @@ fn wrap_text(ctx: &web_sys::CanvasRenderingContext2d, text: &str, max_width: f64 format!("{} {}", current_line, word) }; - let width = ctx.measure_text(&test_line).map(|m| m.width()).unwrap_or(0.0); + let width = ctx + .measure_text(&test_line) + .map(|m| m.width()) + .unwrap_or(0.0); if width > max_width && !current_line.is_empty() { lines.push(current_line); @@ -834,11 +926,7 @@ fn wrap_text(ctx: &web_sys::CanvasRenderingContext2d, text: &str, max_width: f64 /// Returns true if the alpha channel at the clicked pixel is > 0. /// This enables pixel-perfect hit detection on avatar canvases. #[cfg(feature = "hydrate")] -pub fn hit_test_canvas( - canvas: &web_sys::HtmlCanvasElement, - client_x: f64, - client_y: f64, -) -> bool { +pub fn hit_test_canvas(canvas: &web_sys::HtmlCanvasElement, client_x: f64, client_y: f64) -> bool { use wasm_bindgen::JsCast; // Get the canvas bounding rect to transform client coords to canvas coords @@ -849,7 +937,11 @@ pub fn hit_test_canvas( let relative_y = client_y - rect.top(); // Check if click is within canvas bounds - if relative_x < 0.0 || relative_y < 0.0 || relative_x >= rect.width() || relative_y >= rect.height() { + if relative_x < 0.0 + || relative_y < 0.0 + || relative_x >= rect.width() + || relative_y >= rect.height() + { return false; } diff --git a/crates/chattyness-user-ui/src/components/avatar_editor.rs b/crates/chattyness-user-ui/src/components/avatar_editor.rs index 18a5329..b4bb3b6 100644 --- a/crates/chattyness-user-ui/src/components/avatar_editor.rs +++ b/crates/chattyness-user-ui/src/components/avatar_editor.rs @@ -99,8 +99,8 @@ fn RenderedPreview(#[prop(into)] avatar: Signal>) -> imp use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; - use wasm_bindgen::closure::Closure; use wasm_bindgen::JsCast; + use wasm_bindgen::closure::Closure; let image_cache: Rc>> = Rc::new(RefCell::new(HashMap::new())); @@ -134,36 +134,37 @@ fn RenderedPreview(#[prop(into)] avatar: Signal>) -> imp ctx.fill_rect(0.0, 0.0, canvas_size as f64, canvas_size as f64); // Helper to load and draw an image at a grid position - let draw_at_position = |path: &str, - pos: usize, - cache: &Rc>>, - ctx: &web_sys::CanvasRenderingContext2d| { - let normalized_path = normalize_asset_path(path); - let mut cache_borrow = cache.borrow_mut(); - let row = pos / 3; - let col = pos % 3; - let x = (col * cell_size) as f64; - let y = (row * cell_size) as f64; - let size = cell_size as f64; + let draw_at_position = + |path: &str, + pos: usize, + cache: &Rc>>, + ctx: &web_sys::CanvasRenderingContext2d| { + let normalized_path = normalize_asset_path(path); + let mut cache_borrow = cache.borrow_mut(); + let row = pos / 3; + let col = pos % 3; + let x = (col * cell_size) as f64; + let y = (row * cell_size) as f64; + let size = cell_size as f64; - if let Some(img) = cache_borrow.get(&normalized_path) { - if img.complete() && img.natural_width() > 0 { - let _ = ctx.draw_image_with_html_image_element_and_dw_and_dh( - img, x, y, size, size, - ); + if let Some(img) = cache_borrow.get(&normalized_path) { + if img.complete() && img.natural_width() > 0 { + let _ = ctx.draw_image_with_html_image_element_and_dw_and_dh( + img, x, y, size, size, + ); + } + } else { + let img = web_sys::HtmlImageElement::new().unwrap(); + let trigger = set_redraw_trigger; + let onload = Closure::once(Box::new(move || { + trigger.update(|v| *v += 1); + }) as Box); + img.set_onload(Some(onload.as_ref().unchecked_ref())); + onload.forget(); + img.set_src(&normalized_path); + cache_borrow.insert(normalized_path, img); } - } else { - let img = web_sys::HtmlImageElement::new().unwrap(); - let trigger = set_redraw_trigger; - let onload = Closure::once(Box::new(move || { - trigger.update(|v| *v += 1); - }) as Box); - img.set_onload(Some(onload.as_ref().unchecked_ref())); - onload.forget(); - img.set_src(&normalized_path); - cache_borrow.insert(normalized_path, img); - } - }; + }; // Draw layers in order: skin -> clothes -> accessories -> current emotion for (pos, path) in av.skin_layer.iter().enumerate() { @@ -252,7 +253,7 @@ pub fn AvatarEditorPopup( let (context_menu, set_context_menu) = signal(Option::::None); // Saving state - let (saving, set_saving) = signal(false); + let (_saving, set_saving) = signal(false); // Helper to get current layer name for API calls let get_current_layer_name = move || -> String { @@ -290,8 +291,9 @@ pub fn AvatarEditorPopup( let response = Request::get("/api/inventory").send().await; match response { Ok(resp) if resp.ok() => { - if let Ok(data) = - resp.json::().await + if let Ok(data) = resp + .json::() + .await { set_inventory_items.set(data.items); set_inventory_loaded.set(true); @@ -353,8 +355,14 @@ pub fn AvatarEditorPopup( item.layer .map(|l| match (l, layer) { (chattyness_db::models::AvatarLayer::Skin, BaseLayer::Skin) => true, - (chattyness_db::models::AvatarLayer::Clothes, BaseLayer::Clothes) => true, - (chattyness_db::models::AvatarLayer::Accessories, BaseLayer::Accessories) => true, + ( + chattyness_db::models::AvatarLayer::Clothes, + BaseLayer::Clothes, + ) => true, + ( + chattyness_db::models::AvatarLayer::Accessories, + BaseLayer::Accessories, + ) => true, _ => false, }) .unwrap_or(false) diff --git a/crates/chattyness-user-ui/src/components/chat.rs b/crates/chattyness-user-ui/src/components/chat.rs index e93ecca..1ff1f6e 100644 --- a/crates/chattyness-user-ui/src/components/chat.rs +++ b/crates/chattyness-user-ui/src/components/chat.rs @@ -5,7 +5,7 @@ use leptos::prelude::*; use chattyness_db::models::EmotionAvailability; use chattyness_db::ws_messages::ClientMessage; -use super::emotion_picker::{EmoteListPopup, LabelStyle, EMOTIONS}; +use super::emotion_picker::{EMOTIONS, EmoteListPopup, LabelStyle}; use super::ws_client::WsSenderStorage; /// Command mode state for the chat input. @@ -90,13 +90,10 @@ pub fn ChatInput( emotion_availability: Signal>, skin_preview_path: Signal>, focus_trigger: Signal, - #[prop(default = Signal::derive(|| ':'))] - focus_prefix: Signal, + #[prop(default = Signal::derive(|| ':'))] focus_prefix: Signal, on_focus_change: Callback, - #[prop(optional)] - on_open_settings: Option>, - #[prop(optional)] - on_open_inventory: Option>, + #[prop(optional)] on_open_settings: Option>, + #[prop(optional)] on_open_inventory: Option>, /// Signal containing the display name to whisper to. When set, pre-fills the input. #[prop(optional, into)] whisper_target: Option>>, diff --git a/crates/chattyness-user-ui/src/components/chat_types.rs b/crates/chattyness-user-ui/src/components/chat_types.rs index 647d1ee..60b3f14 100644 --- a/crates/chattyness-user-ui/src/components/chat_types.rs +++ b/crates/chattyness-user-ui/src/components/chat_types.rs @@ -96,18 +96,18 @@ pub struct ActiveBubble { /// Returns (background_color, border_color, text_color). pub fn emotion_bubble_colors(emotion: &str) -> (&'static str, &'static str, &'static str) { match emotion { - "neutral" => ("#374151", "#4B5563", "#F9FAFB"), // gray - "happy" => ("#FBBF24", "#F59E0B", "#1F2937"), // amber - "sad" => ("#3B82F6", "#2563EB", "#F9FAFB"), // blue - "angry" => ("#EF4444", "#DC2626", "#F9FAFB"), // red + "neutral" => ("#374151", "#4B5563", "#F9FAFB"), // gray + "happy" => ("#FBBF24", "#F59E0B", "#1F2937"), // amber + "sad" => ("#3B82F6", "#2563EB", "#F9FAFB"), // blue + "angry" => ("#EF4444", "#DC2626", "#F9FAFB"), // red "surprised" => ("#A855F7", "#9333EA", "#F9FAFB"), // purple - "thinking" => ("#6366F1", "#4F46E5", "#F9FAFB"), // indigo - "laughing" => ("#FBBF24", "#F59E0B", "#1F2937"), // amber - "crying" => ("#3B82F6", "#2563EB", "#F9FAFB"), // blue - "love" => ("#EC4899", "#DB2777", "#F9FAFB"), // pink - "confused" => ("#8B5CF6", "#7C3AED", "#F9FAFB"), // violet - "sleeping" => ("#1F2937", "#374151", "#9CA3AF"), // dark gray - "wink" => ("#10B981", "#059669", "#F9FAFB"), // emerald - _ => ("#374151", "#4B5563", "#F9FAFB"), // default - gray + "thinking" => ("#6366F1", "#4F46E5", "#F9FAFB"), // indigo + "laughing" => ("#FBBF24", "#F59E0B", "#1F2937"), // amber + "crying" => ("#3B82F6", "#2563EB", "#F9FAFB"), // blue + "love" => ("#EC4899", "#DB2777", "#F9FAFB"), // pink + "confused" => ("#8B5CF6", "#7C3AED", "#F9FAFB"), // violet + "sleeping" => ("#1F2937", "#374151", "#9CA3AF"), // dark gray + "wink" => ("#10B981", "#059669", "#F9FAFB"), // emerald + _ => ("#374151", "#4B5563", "#F9FAFB"), // default - gray } } diff --git a/crates/chattyness-user-ui/src/components/context_menu.rs b/crates/chattyness-user-ui/src/components/context_menu.rs index e8d432e..236b16a 100644 --- a/crates/chattyness-user-ui/src/components/context_menu.rs +++ b/crates/chattyness-user-ui/src/components/context_menu.rs @@ -85,7 +85,7 @@ pub fn ContextMenu( // Click outside handler #[cfg(feature = "hydrate")] { - use wasm_bindgen::{closure::Closure, JsCast}; + use wasm_bindgen::{JsCast, closure::Closure}; Effect::new(move |_| { if !open.get() { @@ -100,28 +100,35 @@ pub fn ContextMenu( let menu_el: web_sys::HtmlElement = menu_el.into(); let menu_el_clone = menu_el.clone(); - let handler = Closure::::new(move |ev: web_sys::MouseEvent| { - if let Some(target) = ev.target() { - if let Ok(target_el) = target.dyn_into::() { - if !menu_el_clone.contains(Some(&target_el)) { - on_close.run(()); + let handler = + Closure::::new(move |ev: web_sys::MouseEvent| { + if let Some(target) = ev.target() { + if let Ok(target_el) = target.dyn_into::() { + if !menu_el_clone.contains(Some(&target_el)) { + on_close.run(()); + } } } - } - }); + }); let window = web_sys::window().unwrap(); - let _ = window.add_event_listener_with_callback("mousedown", handler.as_ref().unchecked_ref()); + let _ = window + .add_event_listener_with_callback("mousedown", handler.as_ref().unchecked_ref()); // Escape key handler let on_close_esc = on_close.clone(); - let keydown_handler = Closure::::new(move |ev: web_sys::KeyboardEvent| { - if ev.key() == "Escape" { - on_close_esc.run(()); - ev.prevent_default(); - } - }); - let _ = window.add_event_listener_with_callback("keydown", keydown_handler.as_ref().unchecked_ref()); + let keydown_handler = Closure::::new( + move |ev: web_sys::KeyboardEvent| { + if ev.key() == "Escape" { + on_close_esc.run(()); + ev.prevent_default(); + } + }, + ); + let _ = window.add_event_listener_with_callback( + "keydown", + keydown_handler.as_ref().unchecked_ref(), + ); // Store handlers to clean up (they get cleaned up when Effect reruns) handler.forget(); diff --git a/crates/chattyness-user-ui/src/components/conversation_modal.rs b/crates/chattyness-user-ui/src/components/conversation_modal.rs index 465e831..037f82b 100644 --- a/crates/chattyness-user-ui/src/components/conversation_modal.rs +++ b/crates/chattyness-user-ui/src/components/conversation_modal.rs @@ -42,7 +42,8 @@ pub fn ConversationModal( if let Some(input) = input_ref.get() { let _ = input.focus(); } - }) as Box); + }) + as Box); if let Some(window) = web_sys::window() { let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0( diff --git a/crates/chattyness-user-ui/src/components/editor.rs b/crates/chattyness-user-ui/src/components/editor.rs index 0bba83e..9246b5b 100644 --- a/crates/chattyness-user-ui/src/components/editor.rs +++ b/crates/chattyness-user-ui/src/components/editor.rs @@ -85,7 +85,9 @@ pub fn SceneCanvas( let canvas_style = Signal::derive(move || { let w = width.get(); let h = height.get(); - let bg_color = background_color.get().unwrap_or_else(|| "#1a1a2e".to_string()); + let bg_color = background_color + .get() + .unwrap_or_else(|| "#1a1a2e".to_string()); if let Some(img) = background_image.get() { format!( @@ -93,7 +95,10 @@ pub fn SceneCanvas( w, h, img ) } else { - format!("width: {}px; height: {}px; background-color: {};", w, h, bg_color) + format!( + "width: {}px; height: {}px; background-color: {};", + w, h, bg_color + ) } }); @@ -134,7 +139,6 @@ pub fn SceneCanvas( /// Canvas for drawing new spots. #[component] -#[allow(unused_variables)] pub fn SpotDrawer( #[prop(into)] width: Signal, #[prop(into)] height: Signal, @@ -144,17 +148,16 @@ pub fn SpotDrawer( #[prop(into)] background_image: Signal>, #[prop(into)] existing_spots_wkt: Signal>, ) -> impl IntoView { - let (drawing_points, _set_drawing_points) = signal(Vec::<(f64, f64)>::new()); - let (is_drawing, _set_is_drawing) = signal(false); - let (start_point, _set_start_point) = signal(Option::<(f64, f64)>::None); - #[cfg(feature = "hydrate")] - let (set_drawing_points, set_is_drawing, set_start_point) = - (_set_drawing_points, _set_is_drawing, _set_start_point); + let (drawing_points, set_drawing_points) = signal(Vec::<(f64, f64)>::new()); + let (is_drawing, set_is_drawing) = signal(false); + let (start_point, set_start_point) = signal(Option::<(f64, f64)>::None); let canvas_style = Signal::derive(move || { let w = width.get(); let h = height.get(); - let bg_color = background_color.get().unwrap_or_else(|| "#1a1a2e".to_string()); + let bg_color = background_color + .get() + .unwrap_or_else(|| "#1a1a2e".to_string()); if let Some(img) = background_image.get() { format!( @@ -162,20 +165,21 @@ pub fn SpotDrawer( w, h, img ) } else { - format!("width: {}px; height: {}px; background-color: {}; cursor: crosshair;", w, h, bg_color) + format!( + "width: {}px; height: {}px; background-color: {}; cursor: crosshair;", + w, h, bg_color + ) } }); let on_mouse_down = move |ev: leptos::ev::MouseEvent| { #[cfg(feature = "hydrate")] { - let rect = ev - .target() - .and_then(|t| { - use wasm_bindgen::JsCast; - t.dyn_ref::() - .map(|el| el.get_bounding_client_rect()) - }); + let rect = ev.target().and_then(|t| { + use wasm_bindgen::JsCast; + t.dyn_ref::() + .map(|el| el.get_bounding_client_rect()) + }); if let Some(rect) = rect { let x = ev.client_x() as f64 - rect.left(); @@ -202,13 +206,11 @@ pub fn SpotDrawer( { if mode.get() == DrawingMode::Rectangle && is_drawing.get() { if let Some((start_x, start_y)) = start_point.get() { - let rect = ev - .target() - .and_then(|t| { - use wasm_bindgen::JsCast; - t.dyn_ref::() - .map(|el| el.get_bounding_client_rect()) - }); + let rect = ev.target().and_then(|t| { + use wasm_bindgen::JsCast; + t.dyn_ref::() + .map(|el| el.get_bounding_client_rect()) + }); if let Some(rect) = rect { let end_x = ev.client_x() as f64 - rect.left(); @@ -222,7 +224,16 @@ pub fn SpotDrawer( if (max_x - min_x) > 10.0 && (max_y - min_y) > 10.0 { let wkt = format!( "POLYGON(({} {}, {} {}, {} {}, {} {}, {} {}))", - min_x, min_y, max_x, min_y, max_x, max_y, min_x, max_y, min_x, min_y + min_x, + min_y, + max_x, + min_y, + max_x, + max_y, + min_x, + max_y, + min_x, + min_y ); on_complete.run(wkt); } @@ -324,8 +335,14 @@ fn parse_wkt_to_style(wkt: &str) -> String { if points.len() >= 4 { let min_x = points.iter().map(|(x, _)| *x).fold(f64::INFINITY, f64::min); let min_y = points.iter().map(|(_, y)| *y).fold(f64::INFINITY, f64::min); - let max_x = points.iter().map(|(x, _)| *x).fold(f64::NEG_INFINITY, f64::max); - let max_y = points.iter().map(|(_, y)| *y).fold(f64::NEG_INFINITY, f64::max); + let max_x = points + .iter() + .map(|(x, _)| *x) + .fold(f64::NEG_INFINITY, f64::max); + let max_y = points + .iter() + .map(|(_, y)| *y) + .fold(f64::NEG_INFINITY, f64::max); return format!( "left: {}px; top: {}px; width: {}px; height: {}px;", diff --git a/crates/chattyness-user-ui/src/components/emotion_picker.rs b/crates/chattyness-user-ui/src/components/emotion_picker.rs index 1f1a231..4dfe514 100644 --- a/crates/chattyness-user-ui/src/components/emotion_picker.rs +++ b/crates/chattyness-user-ui/src/components/emotion_picker.rs @@ -104,9 +104,7 @@ pub fn EmoteListPopup( if show_all_emotions { EMOTIONS .iter() - .filter(|name| { - filter_text.is_empty() || name.starts_with(&filter_text) - }) + .filter(|name| filter_text.is_empty() || name.starts_with(&filter_text)) .map(|name| ((*name).to_string(), None, true)) .collect() } else { @@ -223,7 +221,7 @@ pub fn EmotionPreview(skin_path: Option, emotion_path: Option) - #[cfg(feature = "hydrate")] { - use wasm_bindgen::{closure::Closure, JsCast}; + use wasm_bindgen::{JsCast, closure::Closure}; let skin_path_clone = skin_path.clone(); let emotion_path_clone = emotion_path.clone(); diff --git a/crates/chattyness-user-ui/src/components/forms.rs b/crates/chattyness-user-ui/src/components/forms.rs index be42c9a..eb0decb 100644 --- a/crates/chattyness-user-ui/src/components/forms.rs +++ b/crates/chattyness-user-ui/src/components/forms.rs @@ -295,7 +295,10 @@ pub fn ColorPicker( /// Color palette component. #[component] -pub fn ColorPalette(#[prop(into)] value: Signal, on_change: Callback) -> impl IntoView { +pub fn ColorPalette( + #[prop(into)] value: Signal, + on_change: Callback, +) -> impl IntoView { let colors = [ "#1a1a2e", "#16213e", "#0f3460", "#e94560", "#533483", "#2c3e50", "#1e8449", "#d35400", ]; @@ -334,7 +337,10 @@ pub fn ColorPalette(#[prop(into)] value: Signal, on_change: Callback bool { use wasm_bindgen::JsCast; ev.target() - .and_then(|t| t.dyn_ref::().map(|el| el.checked())) + .and_then(|t| { + t.dyn_ref::() + .map(|el| el.checked()) + }) .unwrap_or(false) } diff --git a/crates/chattyness-user-ui/src/components/inventory.rs b/crates/chattyness-user-ui/src/components/inventory.rs index b39b7e3..c9eaf67 100644 --- a/crates/chattyness-user-ui/src/components/inventory.rs +++ b/crates/chattyness-user-ui/src/components/inventory.rs @@ -84,8 +84,9 @@ pub fn InventoryPopup( let response = Request::get("/api/inventory").send().await; match response { Ok(resp) if resp.ok() => { - if let Ok(data) = - resp.json::().await + if let Ok(data) = resp + .json::() + .await { set_items.set(data.items); set_my_inventory_loaded.set(true); @@ -123,8 +124,9 @@ pub fn InventoryPopup( let response = Request::get("/api/inventory/server").send().await; match response { Ok(resp) if resp.ok() => { - if let Ok(data) = - resp.json::().await + if let Ok(data) = resp + .json::() + .await { set_server_props.set(data.props); set_server_loaded.set(true); @@ -133,8 +135,10 @@ pub fn InventoryPopup( } } Ok(resp) => { - set_server_error - .set(Some(format!("Failed to load server props: {}", resp.status()))); + set_server_error.set(Some(format!( + "Failed to load server props: {}", + resp.status() + ))); } Err(e) => { set_server_error.set(Some(format!("Network error: {}", e))); @@ -171,8 +175,9 @@ pub fn InventoryPopup( .await; match response { Ok(resp) if resp.ok() => { - if let Ok(data) = - resp.json::().await + if let Ok(data) = resp + .json::() + .await { set_realm_props.set(data.props); set_realm_loaded.set(true); @@ -181,8 +186,10 @@ pub fn InventoryPopup( } } Ok(resp) => { - set_realm_error - .set(Some(format!("Failed to load realm props: {}", resp.status()))); + set_realm_error.set(Some(format!( + "Failed to load realm props: {}", + resp.status() + ))); } Err(e) => { set_realm_error.set(Some(format!("Network error: {}", e))); diff --git a/crates/chattyness-user-ui/src/components/keybindings.rs b/crates/chattyness-user-ui/src/components/keybindings.rs index 37961ec..10c6276 100644 --- a/crates/chattyness-user-ui/src/components/keybindings.rs +++ b/crates/chattyness-user-ui/src/components/keybindings.rs @@ -7,9 +7,8 @@ use crate::utils::LocalStoragePersist; /// Key slot names for the 12 emotion keybindings. /// Maps to e1, e2, ..., e9, e0, eq, ew -pub const KEYBINDING_SLOTS: [&str; 12] = [ - "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "q", "w", -]; +pub const KEYBINDING_SLOTS: [&str; 12] = + ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "q", "w"]; /// Default emotion order for keybinding slots. /// Slot 0 (e1) is always Happy (locked). diff --git a/crates/chattyness-user-ui/src/components/layout.rs b/crates/chattyness-user-ui/src/components/layout.rs index 7776098..9f12aac 100644 --- a/crates/chattyness-user-ui/src/components/layout.rs +++ b/crates/chattyness-user-ui/src/components/layout.rs @@ -189,7 +189,10 @@ pub fn Card(#[prop(optional)] class: &'static str, children: Children) -> impl I #[component] pub fn SceneThumbnail(scene: chattyness_db::models::SceneSummary) -> impl IntoView { let background_style = match (&scene.background_image_path, &scene.background_color) { - (Some(path), _) => format!("background-image: url('{}'); background-size: cover; background-position: center;", path), + (Some(path), _) => format!( + "background-image: url('{}'); background-size: cover; background-position: center;", + path + ), (None, Some(color)) => format!("background-color: {};", color), (None, None) => "background-color: #1a1a2e;".to_string(), }; diff --git a/crates/chattyness-user-ui/src/components/notifications.rs b/crates/chattyness-user-ui/src/components/notifications.rs index f7f0287..f060463 100644 --- a/crates/chattyness-user-ui/src/components/notifications.rs +++ b/crates/chattyness-user-ui/src/components/notifications.rs @@ -75,9 +75,7 @@ pub fn NotificationToast( // Clear any existing timer if let Some(handle) = timer_handle_effect.borrow_mut().take() { - web_sys::window() - .unwrap() - .clear_timeout_with_handle(handle); + web_sys::window().unwrap().clear_timeout_with_handle(handle); } // Start new timer if notification is present @@ -89,13 +87,16 @@ pub fn NotificationToast( let closure = wasm_bindgen::closure::Closure::once(Box::new(move || { on_dismiss.run(id); timer_handle_inner.borrow_mut().take(); - }) as Box); + }) + as Box); if let Some(window) = web_sys::window() { - if let Ok(handle) = window.set_timeout_with_callback_and_timeout_and_arguments_0( - closure.as_ref().unchecked_ref(), - 5000, // 5 second auto-dismiss - ) { + if let Ok(handle) = window + .set_timeout_with_callback_and_timeout_and_arguments_0( + closure.as_ref().unchecked_ref(), + 5000, // 5 second auto-dismiss + ) + { *timer_handle_effect.borrow_mut() = Some(handle); } } @@ -110,8 +111,9 @@ pub fn NotificationToast( use std::cell::RefCell; use std::rc::Rc; - let closure_holder: Rc>>> = - Rc::new(RefCell::new(None)); + let closure_holder: Rc< + RefCell>>, + > = Rc::new(RefCell::new(None)); let closure_holder_clone = closure_holder.clone(); Effect::new(move |_| { @@ -140,37 +142,37 @@ pub fn NotificationToast( let on_history = on_history.clone(); let on_dismiss = on_dismiss.clone(); - let closure = wasm_bindgen::closure::Closure::::new(move |ev: web_sys::KeyboardEvent| { - let key = ev.key(); - match key.as_str() { - "r" | "R" => { - ev.prevent_default(); - on_reply.run(display_name.clone()); - on_dismiss.run(notif_id); + let closure = wasm_bindgen::closure::Closure::::new( + move |ev: web_sys::KeyboardEvent| { + let key = ev.key(); + match key.as_str() { + "r" | "R" => { + ev.prevent_default(); + on_reply.run(display_name.clone()); + on_dismiss.run(notif_id); + } + "c" | "C" => { + ev.prevent_default(); + on_context.run(display_name.clone()); + on_dismiss.run(notif_id); + } + "h" | "H" => { + ev.prevent_default(); + on_history.run(()); + on_dismiss.run(notif_id); + } + "Escape" => { + ev.prevent_default(); + on_dismiss.run(notif_id); + } + _ => {} } - "c" | "C" => { - ev.prevent_default(); - on_context.run(display_name.clone()); - on_dismiss.run(notif_id); - } - "h" | "H" => { - ev.prevent_default(); - on_history.run(()); - on_dismiss.run(notif_id); - } - "Escape" => { - ev.prevent_default(); - on_dismiss.run(notif_id); - } - _ => {} - } - }); + }, + ); if let Some(window) = web_sys::window() { - let _ = window.add_event_listener_with_callback( - "keydown", - closure.as_ref().unchecked_ref(), - ); + let _ = window + .add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref()); } // Store closure for cleanup on next change diff --git a/crates/chattyness-user-ui/src/components/scene_viewer.rs b/crates/chattyness-user-ui/src/components/scene_viewer.rs index 4be0cae..5f19fd2 100644 --- a/crates/chattyness-user-ui/src/components/scene_viewer.rs +++ b/crates/chattyness-user-ui/src/components/scene_viewer.rs @@ -17,11 +17,11 @@ use chattyness_db::models::{ChannelMemberWithAvatar, LooseProp, Scene}; #[cfg(feature = "hydrate")] use super::avatar_canvas::hit_test_canvas; -use super::avatar_canvas::{member_key, AvatarCanvas}; +use super::avatar_canvas::{AvatarCanvas, member_key}; use super::chat_types::ActiveBubble; use super::context_menu::{ContextMenu, ContextMenuItem}; use super::settings::{ - calculate_min_zoom, ViewerSettings, BASE_PROP_SIZE, REFERENCE_HEIGHT, REFERENCE_WIDTH, + BASE_PROP_SIZE, REFERENCE_HEIGHT, REFERENCE_WIDTH, ViewerSettings, calculate_min_zoom, }; use super::ws_client::FadingMember; use crate::utils::parse_bounds_dimensions; @@ -35,18 +35,12 @@ use crate::utils::parse_bounds_dimensions; #[component] pub fn RealmSceneViewer( scene: Scene, - #[allow(unused)] realm_slug: String, - #[prop(into)] - members: Signal>, - #[prop(into)] - active_bubbles: Signal, Option), ActiveBubble>>, - #[prop(into)] - loose_props: Signal>, - #[prop(into)] - on_move: Callback<(f64, f64)>, - #[prop(into)] - on_prop_click: Callback, + #[prop(into)] members: Signal>, + #[prop(into)] active_bubbles: Signal, Option), ActiveBubble>>, + #[prop(into)] loose_props: Signal>, + #[prop(into)] on_move: Callback<(f64, f64)>, + #[prop(into)] on_prop_click: Callback, /// Viewer settings for pan/zoom/enlarge modes. #[prop(optional)] settings: Option>, @@ -104,9 +98,7 @@ pub fn RealmSceneViewer( .clone() .unwrap_or_else(|| "#1a1a2e".to_string()); - #[allow(unused_variables)] let has_background_image = scene.background_image_path.is_some(); - #[allow(unused_variables)] let image_path = scene.background_image_path.clone().unwrap_or_default(); // Canvas refs for background and props layers @@ -217,15 +209,18 @@ pub fn RealmSceneViewer( // Parse the member ID to determine if it's a user_id or guest_session_id if let Ok(member_id) = member_id_str.parse::() { // Check if this is the current user's avatar - let is_current_user = my_user_id == Some(member_id) || - my_guest_session_id == Some(member_id); + let is_current_user = my_user_id == Some(member_id) + || my_guest_session_id == Some(member_id); if !is_current_user { // Find the display name for this member - let display_name = members.get().iter() + let display_name = members + .get() + .iter() .find(|m| { - m.member.user_id == Some(member_id) || - m.member.guest_session_id == Some(member_id) + m.member.user_id == Some(member_id) + || m.member.guest_session_id + == Some(member_id) }) .map(|m| m.member.display_name.clone()); @@ -256,7 +251,7 @@ pub fn RealmSceneViewer( { use std::cell::RefCell; use std::rc::Rc; - use wasm_bindgen::{closure::Closure, JsCast}; + use wasm_bindgen::{JsCast, closure::Closure}; let image_path_clone = image_path.clone(); let bg_color_clone = bg_color.clone(); @@ -395,7 +390,8 @@ pub fn RealmSceneViewer( canvas_width as f64, canvas_height as f64, ); - }) as Box); + }) + as Box); img.set_onload(Some(onload.as_ref().unchecked_ref())); onload.forget(); @@ -419,7 +415,8 @@ pub fn RealmSceneViewer( let canvas_aspect = display_width as f64 / display_height as f64; let scene_aspect = scene_width_f / scene_height_f; - let (draw_width, draw_height, draw_x, draw_y) = if canvas_aspect > scene_aspect { + let (draw_width, draw_height, draw_x, draw_y) = if canvas_aspect > scene_aspect + { let h = display_height as f64; let w = h * scene_aspect; let x = (display_width as f64 - w) / 2.0; @@ -462,9 +459,14 @@ pub fn RealmSceneViewer( let onload = Closure::once(Box::new(move || { let _ = ctx_clone.draw_image_with_html_image_element_and_dw_and_dh( - &img_clone, draw_x, draw_y, draw_width, draw_height, + &img_clone, + draw_x, + draw_y, + draw_width, + draw_height, ); - }) as Box); + }) + as Box); img.set_onload(Some(onload.as_ref().unchecked_ref())); onload.forget(); @@ -498,6 +500,12 @@ pub fn RealmSceneViewer( return; } + // Read scale factors inside the Effect (reactive context) before the closure + let sx = scale_x.get(); + let sy = scale_y.get(); + let ox = offset_x.get(); + let oy = offset_y.get(); + let Some(canvas) = props_canvas_ref.get() else { return; }; @@ -520,12 +528,6 @@ pub fn RealmSceneViewer( // Clear with transparency ctx.clear_rect(0.0, 0.0, canvas_width as f64, canvas_height as f64); - // Get stored scale factors - let sx = scale_x.get(); - let sy = scale_y.get(); - let ox = offset_x.get(); - let oy = offset_y.get(); - // Calculate prop size based on mode let prop_size = calculate_prop_size( current_pan_mode, @@ -582,7 +584,9 @@ pub fn RealmSceneViewer( if canvas_width > 0 && canvas_height > 0 { if let Some(canvas) = props_canvas_ref.get() { let canvas_el: &web_sys::HtmlCanvasElement = &canvas; - if canvas_el.width() != canvas_width || canvas_el.height() != canvas_height { + if canvas_el.width() != canvas_width + || canvas_el.height() != canvas_height + { canvas_el.set_width(canvas_width); canvas_el.set_height(canvas_height); } @@ -631,39 +635,44 @@ pub fn RealmSceneViewer( let last_y_down = last_y.clone(); // Middle mouse down - start drag - let onmousedown = Closure::::new(move |ev: web_sys::MouseEvent| { - // Button 1 is middle mouse button - if ev.button() == 1 { - is_dragging_down.set(true); - last_x_down.set(ev.client_x()); - last_y_down.set(ev.client_y()); - let _ = container_for_down.style().set_property("cursor", "grabbing"); - ev.prevent_default(); - } - }); + let onmousedown = + Closure::::new(move |ev: web_sys::MouseEvent| { + // Button 1 is middle mouse button + if ev.button() == 1 { + is_dragging_down.set(true); + last_x_down.set(ev.client_x()); + last_y_down.set(ev.client_y()); + let _ = container_for_down + .style() + .set_property("cursor", "grabbing"); + ev.prevent_default(); + } + }); // Mouse move - drag scroll - let onmousemove = Closure::::new(move |ev: web_sys::MouseEvent| { - if is_dragging_move.get() { - let dx = last_x_move.get() - ev.client_x(); - let dy = last_y_move.get() - ev.client_y(); - last_x_move.set(ev.client_x()); - last_y_move.set(ev.client_y()); - container_for_move.set_scroll_left(container_for_move.scroll_left() + dx); - container_for_move.set_scroll_top(container_for_move.scroll_top() + dy); - } - }); + let onmousemove = + Closure::::new(move |ev: web_sys::MouseEvent| { + if is_dragging_move.get() { + let dx = last_x_move.get() - ev.client_x(); + let dy = last_y_move.get() - ev.client_y(); + last_x_move.set(ev.client_x()); + last_y_move.set(ev.client_y()); + container_for_move.set_scroll_left(container_for_move.scroll_left() + dx); + container_for_move.set_scroll_top(container_for_move.scroll_top() + dy); + } + }); let container_for_up = container_el.clone(); let is_dragging_up = is_dragging.clone(); // Mouse up - stop drag - let onmouseup = Closure::::new(move |_ev: web_sys::MouseEvent| { - if is_dragging_up.get() { - is_dragging_up.set(false); - let _ = container_for_up.style().set_property("cursor", ""); - } - }); + let onmouseup = + Closure::::new(move |_ev: web_sys::MouseEvent| { + if is_dragging_up.get() { + is_dragging_up.set(false); + let _ = container_for_up.style().set_property("cursor", ""); + } + }); // Add event listeners let _ = container_el.add_event_listener_with_callback( @@ -674,21 +683,20 @@ pub fn RealmSceneViewer( "mousemove", onmousemove.as_ref().unchecked_ref(), ); - let _ = container_el.add_event_listener_with_callback( - "mouseup", - onmouseup.as_ref().unchecked_ref(), - ); + let _ = container_el + .add_event_listener_with_callback("mouseup", onmouseup.as_ref().unchecked_ref()); // Also listen for mouseup on window (in case mouse released outside container) if let Some(window) = web_sys::window() { let is_dragging_window = is_dragging.clone(); let container_for_window = container_el.clone(); - let onmouseup_window = Closure::::new(move |_ev: web_sys::MouseEvent| { - if is_dragging_window.get() { - is_dragging_window.set(false); - let _ = container_for_window.style().set_property("cursor", ""); - } - }); + let onmouseup_window = + Closure::::new(move |_ev: web_sys::MouseEvent| { + if is_dragging_window.get() { + is_dragging_window.set(false); + let _ = container_for_window.style().set_property("cursor", ""); + } + }); let _ = window.add_event_listener_with_callback( "mouseup", onmouseup_window.as_ref().unchecked_ref(), @@ -697,11 +705,12 @@ pub fn RealmSceneViewer( } // Prevent context menu on middle click - let oncontextmenu = Closure::::new(move |ev: web_sys::MouseEvent| { - if ev.button() == 1 { - ev.prevent_default(); - } - }); + let oncontextmenu = + Closure::::new(move |ev: web_sys::MouseEvent| { + if ev.button() == 1 { + ev.prevent_default(); + } + }); let _ = container_el.add_event_listener_with_callback( "auxclick", oncontextmenu.as_ref().unchecked_ref(), @@ -713,7 +722,6 @@ pub fn RealmSceneViewer( onmouseup.forget(); oncontextmenu.forget(); }); - } // Create wheel handler closure for use in view @@ -861,7 +869,8 @@ pub fn RealmSceneViewer( // Create a map of members by key for efficient lookup let members_by_key = Signal::derive(move || { use std::collections::HashMap; - sorted_members.get() + sorted_members + .get() .into_iter() .enumerate() .map(|(idx, m)| (member_key(&m), (idx, m))) @@ -871,7 +880,8 @@ pub fn RealmSceneViewer( // Get the list of member keys - use Memo so it only updates when keys actually change // (not when member data like position changes) let member_keys = Memo::new(move |_| { - sorted_members.get() + sorted_members + .get() .iter() .map(member_key) .collect::>() @@ -1111,7 +1121,6 @@ fn draw_loose_props( offset_y: f64, prop_size: f64, ) { - for prop in props { let x = prop.position_x * scale_x + offset_x; let y = prop.position_y * scale_y + offset_y; diff --git a/crates/chattyness-user-ui/src/components/settings.rs b/crates/chattyness-user-ui/src/components/settings.rs index e484c16..af59aa8 100644 --- a/crates/chattyness-user-ui/src/components/settings.rs +++ b/crates/chattyness-user-ui/src/components/settings.rs @@ -42,10 +42,7 @@ pub fn calculate_min_zoom( viewport_width: f64, viewport_height: f64, ) -> f64 { - if scene_width <= 0.0 - || scene_height <= 0.0 - || viewport_width <= 0.0 - || viewport_height <= 0.0 + if scene_width <= 0.0 || scene_height <= 0.0 || viewport_width <= 0.0 || viewport_height <= 0.0 { return 1.0; } diff --git a/crates/chattyness-user-ui/src/components/settings_popup.rs b/crates/chattyness-user-ui/src/components/settings_popup.rs index f567c0b..8194438 100644 --- a/crates/chattyness-user-ui/src/components/settings_popup.rs +++ b/crates/chattyness-user-ui/src/components/settings_popup.rs @@ -4,7 +4,9 @@ use leptos::ev::MouseEvent; use leptos::prelude::*; use super::modals::Modal; -use super::settings::{calculate_min_zoom, ViewerSettings, ZOOM_MAX, ZOOM_STEP, TEXT_EM_MIN, TEXT_EM_MAX, TEXT_EM_STEP}; +use super::settings::{ + TEXT_EM_MAX, TEXT_EM_MIN, TEXT_EM_STEP, ViewerSettings, ZOOM_MAX, ZOOM_STEP, calculate_min_zoom, +}; use crate::utils::LocalStoragePersist; /// Settings popup component for scene viewer configuration. diff --git a/crates/chattyness-user-ui/src/components/ws_client.rs b/crates/chattyness-user-ui/src/components/ws_client.rs index da6f174..a162edc 100644 --- a/crates/chattyness-user-ui/src/components/ws_client.rs +++ b/crates/chattyness-user-ui/src/components/ws_client.rs @@ -6,12 +6,12 @@ use leptos::prelude::*; use leptos::reactive::owner::LocalStorage; -use chattyness_db::models::{ChannelMemberWithAvatar, LooseProp}; #[cfg(feature = "hydrate")] use chattyness_db::models::EmotionState; +use chattyness_db::models::{ChannelMemberWithAvatar, LooseProp}; use chattyness_db::ws_messages::ClientMessage; #[cfg(feature = "hydrate")] -use chattyness_db::ws_messages::{DisconnectReason, ServerMessage, WsConfig}; +use chattyness_db::ws_messages::{DisconnectReason, ServerMessage}; use super::chat_types::ChatMessage; @@ -91,7 +91,7 @@ pub fn use_channel_websocket( ) -> (Signal, WsSenderStorage) { use std::cell::RefCell; use std::rc::Rc; - use wasm_bindgen::{closure::Closure, JsCast}; + use wasm_bindgen::{JsCast, closure::Closure}; use web_sys::{CloseEvent, ErrorEvent, MessageEvent, WebSocket}; let (ws_state, set_ws_state) = signal(WsState::Disconnected); @@ -100,8 +100,8 @@ pub fn use_channel_websocket( // Create a stored sender function (using new_local for WASM single-threaded environment) let ws_ref_for_send = ws_ref.clone(); - let sender: WsSenderStorage = StoredValue::new_local(Some(Box::new( - move |msg: ClientMessage| { + let sender: WsSenderStorage = + StoredValue::new_local(Some(Box::new(move |msg: ClientMessage| { if let Some(ws) = ws_ref_for_send.borrow().as_ref() { if ws.ready_state() == WebSocket::OPEN { if let Ok(json) = serde_json::to_string(&msg) { @@ -111,8 +111,7 @@ pub fn use_channel_websocket( } } } - }, - ))); + }))); // Effect to manage WebSocket lifecycle let ws_ref_clone = ws_ref.clone(); @@ -198,24 +197,38 @@ pub fn use_channel_websocket( if let Ok(msg) = serde_json::from_str::(&text) { // Check for Welcome message to start heartbeat with server-provided config - if let ServerMessage::Welcome { ref config, ref member, .. } = msg { + if let ServerMessage::Welcome { + ref config, + ref member, + .. + } = msg + { if !*heartbeat_started_clone.borrow() { *heartbeat_started_clone.borrow_mut() = true; let ping_interval_ms = config.ping_interval_secs * 1000; let ws_ref_ping = ws_ref_for_heartbeat.clone(); #[cfg(debug_assertions)] web_sys::console::log_1( - &format!("[WS] Starting heartbeat with interval {}ms", ping_interval_ms).into(), + &format!( + "[WS] Starting heartbeat with interval {}ms", + ping_interval_ms + ) + .into(), ); - let heartbeat = gloo_timers::callback::Interval::new(ping_interval_ms as u32, move || { - if let Some(ws) = ws_ref_ping.borrow().as_ref() { - if ws.ready_state() == WebSocket::OPEN { - if let Ok(json) = serde_json::to_string(&ClientMessage::Ping) { - let _ = ws.send_with_str(&json); + let heartbeat = gloo_timers::callback::Interval::new( + ping_interval_ms as u32, + move || { + if let Some(ws) = ws_ref_ping.borrow().as_ref() { + if ws.ready_state() == WebSocket::OPEN { + if let Ok(json) = + serde_json::to_string(&ClientMessage::Ping) + { + let _ = ws.send_with_str(&json); + } } } - } - }); + }, + ); std::mem::forget(heartbeat); } // Call on_welcome callback with current user info @@ -292,7 +305,7 @@ fn handle_server_message( ServerMessage::Welcome { member: _, members: initial_members, - config: _, // Config is handled in the caller for heartbeat setup + config: _, // Config is handled in the caller for heartbeat setup } => { *members_vec = initial_members; on_update.run(members_vec.clone()); @@ -314,7 +327,9 @@ fn handle_server_message( // Find the member before removing let leaving_member = members_vec .iter() - .find(|m| m.member.user_id == user_id && m.member.guest_session_id == guest_session_id) + .find(|m| { + m.member.user_id == user_id && m.member.guest_session_id == guest_session_id + }) .cloned(); // Always remove from active members list diff --git a/crates/chattyness-user-ui/src/lib.rs b/crates/chattyness-user-ui/src/lib.rs index 0d75548..2b0cfe1 100644 --- a/crates/chattyness-user-ui/src/lib.rs +++ b/crates/chattyness-user-ui/src/lib.rs @@ -1,4 +1,9 @@ #![recursion_limit = "256"] +// Server builds don't use browser-specific code guarded by #[cfg(feature = "hydrate")] +#![cfg_attr( + not(feature = "hydrate"), + allow(unused_variables, dead_code, unused_imports) +)] //! User UI components for chattyness. //! //! This crate provides the public user-facing interface including: @@ -30,7 +35,7 @@ pub mod pages; pub mod routes; pub mod utils; -pub use app::{shell, App}; +pub use app::{App, shell}; pub use routes::UserRoutes; #[cfg(feature = "ssr")] diff --git a/crates/chattyness-user-ui/src/pages/home.rs b/crates/chattyness-user-ui/src/pages/home.rs index 5ff180c..c292820 100644 --- a/crates/chattyness-user-ui/src/pages/home.rs +++ b/crates/chattyness-user-ui/src/pages/home.rs @@ -71,7 +71,11 @@ pub fn HomePage() -> impl IntoView { /// Feature card component. #[component] -fn FeatureCard(icon: &'static str, title: &'static str, description: &'static str) -> impl IntoView { +fn FeatureCard( + icon: &'static str, + title: &'static str, + description: &'static str, +) -> impl IntoView { let icon_symbol = match icon { "castle" => "castle", "users" => "users", diff --git a/crates/chattyness-user-ui/src/pages/login.rs b/crates/chattyness-user-ui/src/pages/login.rs index fb4131c..8b53580 100644 --- a/crates/chattyness-user-ui/src/pages/login.rs +++ b/crates/chattyness-user-ui/src/pages/login.rs @@ -67,7 +67,9 @@ fn RealmLoginForm() -> impl IntoView { struct ListResponse { realms: Vec, } - let response = Request::get("/api/realms?include_nsfw=false&limit=20").send().await; + let response = Request::get("/api/realms?include_nsfw=false&limit=20") + .send() + .await; match response { Ok(resp) if resp.ok() => resp.json::().await.ok().map(|r| r.realms), _ => None, diff --git a/crates/chattyness-user-ui/src/pages/password_reset.rs b/crates/chattyness-user-ui/src/pages/password_reset.rs index 0e326ce..66e2ec7 100644 --- a/crates/chattyness-user-ui/src/pages/password_reset.rs +++ b/crates/chattyness-user-ui/src/pages/password_reset.rs @@ -76,7 +76,8 @@ pub fn PasswordResetPage() -> impl IntoView { Ok(resp) => { let status = resp.status(); if status == 401 { - set_error.set(Some("Session expired. Please log in again.".to_string())); + set_error + .set(Some("Session expired. Please log in again.".to_string())); } else if status == 400 { set_error.set(Some("Invalid password. Please try again.".to_string())); } else { diff --git a/crates/chattyness-user-ui/src/pages/realm.rs b/crates/chattyness-user-ui/src/pages/realm.rs index 5789e54..38f9609 100644 --- a/crates/chattyness-user-ui/src/pages/realm.rs +++ b/crates/chattyness-user-ui/src/pages/realm.rs @@ -19,8 +19,8 @@ use crate::components::{ }; #[cfg(feature = "hydrate")] use crate::components::{ - add_to_history, use_channel_websocket, ChannelMemberInfo, ChatMessage, - DEFAULT_BUBBLE_TIMEOUT_MS, FADE_DURATION_MS, HistoryEntry, WsError, + ChannelMemberInfo, ChatMessage, DEFAULT_BUBBLE_TIMEOUT_MS, FADE_DURATION_MS, HistoryEntry, + WsError, add_to_history, use_channel_websocket, }; use crate::utils::LocalStoragePersist; #[cfg(feature = "hydrate")] @@ -32,6 +32,7 @@ use chattyness_db::models::{ #[cfg(feature = "hydrate")] use chattyness_db::ws_messages::ClientMessage; +#[cfg(not(feature = "hydrate"))] use crate::components::ws_client::WsSender; /// Realm landing page component. @@ -102,7 +103,8 @@ pub fn RealmPage() -> impl IntoView { let (whisper_target, set_whisper_target) = signal(Option::::None); // Notification state for cross-scene whispers - let (current_notification, set_current_notification) = signal(Option::::None); + let (current_notification, set_current_notification) = + signal(Option::::None); let (history_modal_open, set_history_modal_open) = signal(false); let (conversation_modal_open, set_conversation_modal_open) = signal(false); let (conversation_partner, set_conversation_partner) = signal(String::new()); @@ -414,7 +416,7 @@ pub fn RealmPage() -> impl IntoView { { use std::cell::RefCell; use std::rc::Rc; - use wasm_bindgen::{closure::Closure, JsCast}; + use wasm_bindgen::{JsCast, closure::Closure}; let closure_holder: Rc>>> = Rc::new(RefCell::new(None)); @@ -512,7 +514,9 @@ pub fn RealmPage() -> impl IntoView { if let Some((dx, dy)) = scroll_delta { // Find the scene container and scroll it if let Some(document) = web_sys::window().and_then(|w| w.document()) { - if let Some(container) = document.query_selector(".scene-container").ok().flatten() { + if let Some(container) = + document.query_selector(".scene-container").ok().flatten() + { let container_el: web_sys::Element = container; container_el.scroll_by_with_x_and_y(dx, dy); } diff --git a/crates/chattyness-user-ui/src/pages/signup.rs b/crates/chattyness-user-ui/src/pages/signup.rs index 6b33cb1..c2cef97 100644 --- a/crates/chattyness-user-ui/src/pages/signup.rs +++ b/crates/chattyness-user-ui/src/pages/signup.rs @@ -45,7 +45,9 @@ pub fn SignupPage() -> impl IntoView { struct ListResponse { realms: Vec, } - let response = Request::get("/api/realms?include_nsfw=false&limit=20").send().await; + let response = Request::get("/api/realms?include_nsfw=false&limit=20") + .send() + .await; match response { Ok(resp) if resp.ok() => resp.json::().await.ok().map(|r| r.realms), _ => None, @@ -74,7 +76,9 @@ pub fn SignupPage() -> impl IntoView { let slug = realm_slug.get(); if slug.is_none() { - set_error.set(Some("Please select a realm or enter a realm name".to_string())); + set_error.set(Some( + "Please select a realm or enter a realm name".to_string(), + )); return; } diff --git a/crates/chattyness-user-ui/src/routes.rs b/crates/chattyness-user-ui/src/routes.rs index c3013f4..b6febbe 100644 --- a/crates/chattyness-user-ui/src/routes.rs +++ b/crates/chattyness-user-ui/src/routes.rs @@ -11,8 +11,8 @@ use leptos::prelude::*; use leptos_router::{ - components::{Route, Routes}, ParamSegment, StaticSegment, + components::{Route, Routes}, }; use crate::pages::{HomePage, LoginPage, PasswordResetPage, RealmPage, SignupPage}; diff --git a/crates/chattyness-user-ui/src/utils.rs b/crates/chattyness-user-ui/src/utils.rs index 384c81e..74a3c25 100644 --- a/crates/chattyness-user-ui/src/utils.rs +++ b/crates/chattyness-user-ui/src/utils.rs @@ -5,7 +5,7 @@ //! - localStorage persistence trait //! - Common hooks (escape key handling) -use serde::{de::DeserializeOwned, Serialize}; +use serde::{Serialize, de::DeserializeOwned}; // ============================================================================ // Geometry Utilities @@ -163,7 +163,7 @@ pub fn use_escape_key( on_close: leptos::prelude::Callback<()>, ) { use leptos::prelude::*; - use wasm_bindgen::{closure::Closure, JsCast}; + use wasm_bindgen::{JsCast, closure::Closure}; Effect::new(move |_| { if !open.get() { @@ -179,8 +179,8 @@ pub fn use_escape_key( }); if let Some(window) = web_sys::window() { - let _ = - window.add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref()); + let _ = window + .add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref()); } // Note: We intentionally don't clean up the listener here. @@ -225,7 +225,10 @@ mod tests { #[test] fn test_normalize_asset_path() { assert_eq!(normalize_asset_path("/images/foo.png"), "/images/foo.png"); - assert_eq!(normalize_asset_path("images/foo.png"), "/static/images/foo.png"); + assert_eq!( + normalize_asset_path("images/foo.png"), + "/static/images/foo.png" + ); assert_eq!(normalize_asset_path("foo.png"), "/static/foo.png"); } }