use leptos::prelude::*; use profile_settings::ProfileSettings; use crate::{ components::{icon::IconComponent, modal::Modal}, icon::Icon, }; mod profile_settings { use filamento::{ error::{AvatarPublishError, CommandError, NickError}, user::User, }; use leptos::prelude::*; use thiserror::Error; use web_sys::{ Event, FileReader, HtmlInputElement, ProgressEvent, Url, js_sys::Uint8Array, wasm_bindgen::{JsCast, UnwrapThrowExt, prelude::Closure}, }; use crate::{ client::Client, files::Files, user::{NO_AVATAR, fetch_avatar}, }; #[derive(Debug, Clone, Error)] pub enum ProfileSaveError { #[error("avatar publish: {0}")] Avatar(#[from] CommandError>), #[error("nick publish: {0}")] Nick(#[from] CommandError), } #[component] pub fn ProfileSettings() -> impl IntoView { let client: Client = use_context().expect("no client in context"); let old_profile = LocalResource::new(move || { let value = client.clone(); async move { // TODO: error let jid = &*value.jid; let old_profile = value.get_user(jid.clone()).await.unwrap(); old_profile } }); view! { {move || { if let Some(old_profile) = old_profile.get() { view! { }.into_any() } else { view! {}.into_any() } }} } } #[component] pub fn ProfileForm(old_profile: User) -> 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::); let error_message = move || { error.with(|error| { if let Some(error) = error { view! {
{error.to_string()}
}.into_any() } else { view! {}.into_any() } }) }; let (success_message, set_success_message) = signal(None::); let success_message = move || { if let Some(message) = success_message.get() { view! {
{message}
}.into_any() } else { view! {}.into_any() } }; let (profile_save_pending, set_profile_save_pending) = signal(false); let profile_upload_data = RwSignal::new(None::>); let new_nick = RwSignal::new(old_profile.nick.clone().unwrap_or_default().to_string()); let has_avatar = RwSignal::new(old_profile.avatar.is_some()); let new_avatar_preview_url = RwSignal::new(None::); let remove_avatar = RwSignal::new(false); let from_input = move |ev: Event| { let elem = ev.target().unwrap().unchecked_into::(); // 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 url = Url::create_object_url_with_blob(&file).unwrap_throw(); new_avatar_preview_url.set(Some(url)); 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 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(); let old_nick = old_profile.nick.clone(); async move { set_profile_save_pending.set(true); let new_nick = new_nick.get(); let new_nick = if new_nick.is_empty() { None } else { Some(new_nick) }; if new_nick != old_nick { match client.change_nick(new_nick).await { Ok(_) => {} Err(e) => { set_error.set(Some(ProfileSaveError::Nick(e))); set_profile_save_pending.set(false); return; } } } if let Some(profile_data) = profile_upload_data.get() { match client.change_avatar(Some(profile_data)).await { Ok(_) => {} Err(e) => { set_error.set(Some(ProfileSaveError::Avatar(e))); set_profile_save_pending.set(false); return; } } } else if remove_avatar.get() { match client.change_avatar(None).await { Ok(_) => {} Err(e) => { set_error.set(Some(ProfileSaveError::Avatar(e))); set_profile_save_pending.set(false); return; } } } set_profile_save_pending.set(false); set_error.set(None); set_success_message.set(Some("Profile Updated!".to_string())); } }); let _old_account_avatar = LocalResource::new(move || { let avatar = old_profile.avatar.clone(); async move { let url = fetch_avatar(avatar.as_deref()).await; new_avatar_preview_url.set(Some(url)); } }); view! {

Profile Preview

{move || { let nick = new_nick.get(); if nick.is_empty() { old_profile.jid.to_string() } else { nick } }}
{success_message} {error_message}

Nick

Avatar

{move || { if has_avatar.get() { view! { Remove Avatar } .into_any() } else { view! {}.into_any() } }}

} } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SettingsPage { Account, Chat, Profile, Privacy, } #[component] pub fn Settings() -> impl IntoView { let show_settings: RwSignal> = use_context().unwrap(); view! {

Settings

Account
Chat
Privacy
Profile
{move || { if let Some(page) = show_settings.get() { match page { SettingsPage::Account => { view! {
"Account settings coming soon!"
} .into_any() } SettingsPage::Chat => { view! {
"Chat settings coming soon!"
} .into_any() } SettingsPage::Profile => { view! { }.into_any() } SettingsPage::Privacy => { view! {
"Privacy settings coming soon!"
} .into_any() } } } else { view! {}.into_any() } }}
} }