diff options
| -rw-r--r-- | Cargo.toml | 3 | ||||
| -rw-r--r-- | assets/style.scss | 73 | ||||
| -rw-r--r-- | src/views/macaw/settings.rs | 173 | 
3 files changed, 227 insertions, 22 deletions
@@ -11,6 +11,7 @@ console_error_panic_hook = "0.1.7"  filamento = { path = "../luz/filamento", features = [  	"reactive_stores",  	"opfs", +	# "serde"  ] }  futures = "0.3.31"  indexmap = "2.9.0" @@ -29,7 +30,7 @@ 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"] } +web-sys = { version = "0.3.77", features = ["HtmlInputElement", "FileList", "Url"] }  [patch.crates-io]  tokio_with_wasm = { path = "../tokio-with-wasm/tokio_with_wasm" } diff --git a/assets/style.scss b/assets/style.scss index 6903145..8838937 100644 --- a/assets/style.scss +++ b/assets/style.scss @@ -102,6 +102,7 @@ label {  input[type="text"],  input[type="password"] { +  font-size: 1rem;    border: 2px solid black;    font-family: "k2d";  } @@ -118,6 +119,7 @@ main {  button,  .button { +  font-size: 1rem;    background: #FACC34;    color: #000000;    text-decoration: none !important; @@ -127,6 +129,7 @@ button,    font-family: K2D;    font-weight: 600;    box-shadow: inset 0px -0.75em 0.5em #ebb62e, 0 0.25em 0.25em #00000048; +  cursor: pointer;  }  h1, @@ -656,6 +659,76 @@ background: #00000060;  .settings-page {    flex: 1 1 0; +  overflow-y: scroll; +} + +.profile-settings { +  display: flex; +  justify-content: flex-end; +  flex-wrap: wrap; +  flex-direction: row-reverse; +} + +.profile-form { +  flex: 1 1 auto; +  margin: 16px; +  display: flex; +  flex-direction: column; +  align-items: flex-start; +  gap: 16px; +} + +.profile-form label { +  font-size: 1rem; +} + +.profile-form .change-avatar { +  display: flex; +  align-items: center; +  flex-wrap: wrap; +  gap: 16px; +} + +#client-user-avatar { +  opacity: 0; +  position: absolute; +  z-index: -1; +} + +.profile-preview { +  margin: 24px; +  border: 2px solid black; +  min-width: 240px; +} + +.profile-preview .preview { +  display: flex; +  flex-direction: column; +  gap: 8px; +} + +.profile-preview h2 { +  padding: 8px 16px; +  border-bottom: 2px solid black; +} + +.profile-preview .avatar { +  width: 64px; +  height: 64px; +} + +.profile-preview .nick { +  font-size: 20px; +  font-weight: bold; +} + +hr { +  width: 100%; +  border: 1px solid black; +} + +.profile-preview .preview { +  padding: 16px;  }  .modal .overlay { diff --git a/src/views/macaw/settings.rs b/src/views/macaw/settings.rs index 11a3fc3..c4cc99b 100644 --- a/src/views/macaw/settings.rs +++ b/src/views/macaw/settings.rs @@ -4,23 +4,53 @@ use profile_settings::ProfileSettings;  use crate::{components::{icon::IconComponent, modal::Modal}, icon::Icon};  mod profile_settings { -    use filamento::error::{AvatarPublishError, CommandError}; +    use filamento::{error::{AvatarPublishError, CommandError, NickError}, user::User};      use thiserror::Error;      use leptos::prelude::*; -    use web_sys::{js_sys::Uint8Array, wasm_bindgen::{prelude::Closure, JsCast, UnwrapThrowExt}, Event, FileReader, HtmlInputElement, ProgressEvent}; +    use web_sys::{js_sys::Uint8Array, wasm_bindgen::{prelude::Closure, JsCast, UnwrapThrowExt}, Event, FileReader, HtmlInputElement, ProgressEvent, Url}; -    use crate::{client::Client, files::Files}; +    use crate::{client::Client, files::Files, user::{fetch_avatar, NO_AVATAR}};      #[derive(Debug, Clone, Error)]      pub enum ProfileSaveError {          #[error("avatar publish: {0}")]          Avatar(#[from] CommandError<AvatarPublishError<Files>>), +        #[error("nick publish: {0}")] +        Nick(#[from] CommandError<NickError>),      }      #[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! { +                        <ProfileForm old_profile /> +                    }.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::<ProfileSaveError>);          let error_message = move || { @@ -32,9 +62,24 @@ mod profile_settings {                  }              })          }; + +        let (success_message, set_success_message) = signal(None::<String>); +        let success_message = move || { +            if let Some(message) = success_message.get() { +                view! { <div class="success">{message}</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 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::<String>); +        let remove_avatar = RwSignal::new(false); +          let from_input = move |ev: Event| {              let elem = ev.target().unwrap().unchecked_into::<HtmlInputElement>(); @@ -43,6 +88,9 @@ mod profile_settings {              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(); @@ -92,32 +140,115 @@ mod profile_settings {          let save_profile = Action::new_local(move |_| {              let client = client.clone(); -            async move {} +            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_success_message.set(Some("Profile Updated!".to_string())); +            }          }); -        let new_nick= RwSignal::new("".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! {              <div class="profile-settings"> -                <form on:submit=move |ev| { -                        ev.prevent_default(); -                        save_profile.dispatch(()); +                <div class="profile-preview"> +                    <h2>Profile Preview</h2> +                    <div class="preview"> +                        <img class="avatar" src=new_avatar_preview_url /> +                        <div class="nick">{move || { +                            let nick = new_nick.get(); +                            if nick.is_empty() { +                                old_profile.jid.to_string() +                            } else { +                                nick +                            } +                        }}</div> +                    </div> +                </div> +                <form class="profile-form" on:submit=move |ev| { +                    ev.prevent_default(); +                    save_profile.dispatch(());                  }> +                    {success_message}                      {error_message} -                    <div class="change-avatar"> -                        <input type="file" id="client-user-avatar" on:change=from_input /> +                    <div> +                    <h3>Nick</h3> +                    <input disabled=profile_save_pending placeholder="Nick" type="text" id="client-user-nick" bind:value=new_nick name="client-user-nick" /> +                    </div> +                    <div> +                        <h3>Avatar</h3> +                        <div class="change-avatar"> +                            <label for="client-user-avatar"><div class="button">Change Avatar</div></label> +                            <input type="file" id="client-user-avatar" on:change=move |e| { +                                has_avatar.set(true); +                                remove_avatar.set(false); +                                from_input(e); +                            } /> +                            {move || { +                                if has_avatar.get() { +                                    view! { +                                        <a on:click=move |_| { +                                            profile_upload_data.set(None); +                                            remove_avatar.set(true); +                                            has_avatar.set(false); +                                            new_avatar_preview_url.set(Some(NO_AVATAR.to_string())); +                                        } style="cursor: pointer">Remove Avatar</a> +                                    }.into_any() +                                } else { +                                    view! {}.into_any() +                                } +                            }} +                        </div>                      </div> -                    <input disabled=profile_save_pending placeholder="Nickname" type="text" id="client-user-nick" bind:value=new_nick name="client-user-nick" /> +                    <hr />                      <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>          }      }  } @@ -153,10 +284,10 @@ pub fn Settings() -> impl IntoView {                      <div class="settings-page">                          {move || if let Some(page) = show_settings.get() {                              match page { -                            SettingsPage::Account => view! { <div>"account"</div> }.into_any(), -                            SettingsPage::Chat => view! { <div>"chat"</div> }.into_any(), +                            SettingsPage::Account => view! { <div style="padding: 16px">"Account settings coming soon!"</div> }.into_any(), +                            SettingsPage::Chat => view! { <div style="padding: 16px">"Chat settings coming soon!"</div> }.into_any(),                              SettingsPage::Profile => view! { <ProfileSettings /> }.into_any(), -                            SettingsPage::Privacy => view! { <div>"privacy"</div> }.into_any(), +                            SettingsPage::Privacy => view! { <div style="padding: 16px">"Privacy settings coming soon!"</div> }.into_any(),                              }                          } else {                              view! {}.into_any()  | 
