diff options
author | 2025-06-01 15:07:03 +0100 | |
---|---|---|
committer | 2025-06-01 15:07:03 +0100 | |
commit | f76c80c1d23177ab00c81240ee3a75d3bcda0e3b (patch) | |
tree | a49b1be04380017b108ec7a9ab4ba61ae236826c | |
parent | 0841bc1c64926de1d1a658ea1498f22e43ac6994 (diff) | |
download | macaw-web-f76c80c1d23177ab00c81240ee3a75d3bcda0e3b.tar.gz macaw-web-f76c80c1d23177ab00c81240ee3a75d3bcda0e3b.tar.bz2 macaw-web-f76c80c1d23177ab00c81240ee3a75d3bcda0e3b.zip |
WIP: profile update
-rw-r--r-- | Cargo.lock | 1 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | src/lib.rs | 113 |
3 files changed, 111 insertions, 5 deletions
@@ -1723,6 +1723,7 @@ dependencies = [ "tracing", "tracing-wasm", "uuid", + "web-sys", ] [[package]] @@ -28,6 +28,8 @@ tokio = { version = "1.44.2", features = ["sync", "rt"] } tracing = "0.1.41" tracing-wasm = "0.2.1" uuid = { version = "1.16.0", features = ["v4"] } +# wasm-bindgen = "0.2.100" +web-sys = { version = "0.3.77", features = ["HtmlInputElement", "FileList"] } [patch.crates-io] tokio_with_wasm = { path = "../tokio-with-wasm/tokio_with_wasm" } @@ -14,7 +14,7 @@ use std::{ use base64::{Engine, prelude::BASE64_STANDARD}; use chrono::{Local, NaiveDateTime, TimeDelta, Utc}; use filamento::{ - chat::{Body, Chat, ChatStoreFields, Delivery, Message, MessageStoreFields}, db::Db, error::{CommandError, ConnectionError, DatabaseError, SubscribeError}, files::{opfs::OPFSError, FileStore, FilesMem, FilesOPFS}, presence::{Offline, Online, Presence, PresenceType, Show}, roster::{Contact, ContactStoreFields}, user::{User, UserStoreFields}, UpdateMessage + chat::{Body, Chat, ChatStoreFields, Delivery, Message, MessageStoreFields}, db::Db, error::{AvatarPublishError, CommandError, ConnectionError, DatabaseError, SubscribeError}, files::{opfs::OPFSError, FileStore, FilesMem, FilesOPFS}, presence::{Offline, Online, Presence, PresenceType, Show}, roster::{Contact, ContactStoreFields}, user::{User, UserStoreFields}, UpdateMessage }; use futures::stream::StreamExt; use indexmap::IndexMap; @@ -23,7 +23,7 @@ use leptos::{ ev::{Event, KeyboardEvent, MouseEvent, SubmitEvent}, html::{self, Div, Input, Pre, Textarea}, prelude::*, - tachys::{dom::document, reactive_graph::bind::GetValue}, + tachys::{dom::document, reactive_graph::bind::GetValue, renderer::dom::Element}, task::{spawn, spawn_local}, }; use reactive_stores::{ArcStore, Store, StoreField}; @@ -35,6 +35,7 @@ use tokio::sync::{ }; use tracing::{debug, error}; use uuid::Uuid; +use web_sys::{js_sys::Uint8Array, wasm_bindgen::{prelude::Closure, JsCast, UnwrapThrowExt}, FileReader, HtmlInputElement, ProgressEvent}; const NO_AVATAR: &str = "/assets/no-avatar.png"; @@ -51,7 +52,7 @@ pub struct Client { file_store: Files, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum Files { Mem(FilesMem), Opfs(FilesOPFS), @@ -1133,11 +1134,113 @@ pub fn Settings() -> impl IntoView { } } +#[derive(Debug, Clone, Error)] +pub enum ProfileSaveError { + #[error("avatar publish: {0}")] + Avatar(#[from] CommandError<AvatarPublishError<Files>>), +} + #[component] pub fn ProfileSettings() -> impl IntoView { + let client: Client = use_context().expect("no client in context"); + + // TODO: compartmentalise into error component, form component... + let (error, set_error) = signal(None::<ProfileSaveError>); + let error_message = move || { + error.with(|error| { + if let Some(error) = error { + view! { <div class="error">{error.to_string()}</div> }.into_any() + } else { + view! {}.into_any() + } + }) + }; + let (profile_save_pending, set_profile_save_pending) = signal(false); + + let profile_upload_data = RwSignal::new(None::<Vec<u8>>); + let from_input = move |ev: Event| { + let elem = ev.target().unwrap().unchecked_into::<HtmlInputElement>(); + + // let UploadSignal(file_signal) = expect_context(); + // file_signal.update(Vec::clear); // Clear list from previous change + let files = elem.files().unwrap_throw(); + + if let Some(file) = files.get(0) { + let reader = FileReader::new().unwrap_throw(); + // let name = file.name(); + + // This closure only needs to be called a single time as we will just + // remake it on each loop + // * web_sys drops it for us when using this specific constructor + let read_file = { + // FileReader is cloned prior to moving into the closure + let reader = reader.to_owned(); + Closure::once_into_js(move |_: ProgressEvent| { + // `.result` valid after the `read_*` completes on FileReader + // https://developer.mozilla.org/en-US/docs/Web/API/FileReader/result + let result = reader.result().unwrap_throw(); + let data= Uint8Array::new(&result).to_vec(); + // Do whatever you want with the Vec<u8> + profile_upload_data.set(Some(data)); + }) + }; + reader.set_onloadend(Some(read_file.as_ref().unchecked_ref())); + + // read_as_array_buffer takes a &Blob + // + // Per https://w3c.github.io/FileAPI/#file-section + // > A File object is a Blob object with a name attribute.. + // + // File is a subclass (inherits) from the Blob interface, so a File + // can be used anywhere a Blob is required. + reader.read_as_array_buffer(&file).unwrap_throw(); + + // You can also use `.read_as_text(&file)` instead if you just want a string. + // This example shows how to extract an array buffer as it is more flexible + // + // If you use `.read_as_text` change the closure from ... + // + // let result = reader.result().unwrap_throw(); + // let vec_of_u8_bytes = Uint8Array::new(&result).to_vec(); + // let content = String::from_utf8(vec_of_u8_bytes).unwrap_throw(); + // + // to ... + // + // let result = reader.result().unwrap_throw(); + // let content = result.as_string().unwrap_throw(); + } else { + profile_upload_data.set(None); + } + }; + + let save_profile = Action::new_local(move |_| { + let client = client.clone(); + async move {} + }); + + let new_nick= RwSignal::new("".to_string()); + view! { - <form> - </form> + <div class="profile-settings"> + <form on:submit=move |ev| { + ev.prevent_default(); + save_profile.dispatch(()); + }> + {error_message} + <div class="change-avatar"> + <input type="file" id="client-user-avatar" on:change=from_input /> + </div> + <input disabled=profile_save_pending placeholder="Nickname" type="text" id="client-user-nick" bind:value=new_nick name="client-user-nick" /> + <input disabled=profile_save_pending class="button" type="submit" value="Save Changes" /> + </form> + </div> + <div class="profile-preview"> + <h2>Profile Preview</h2> + <div class="preview"> + <img /> + <div>nick</div> + </div> + </div> } } |