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>      }  }  | 
