summaryrefslogblamecommitdiffstats
path: root/src/views/macaw/settings.rs
blob: c4cc99bec294675e4222fc372e110473d638009e (plain) (tree)
1
2
3
4
5
6
7
8
9





                                                                         
                                                                                      

                           
                                                                                                                                                         
 
                                                                               




                                                                

                                              





                                                                          



























                                                                             










                                                                                     









                                                                            


                                                                             




                                                                                              







                                                                                 


                                                                                 
















































                                                                                             











































                                                                              

           






                                                                


                                          
















                                                                         
                  
                                     
                                   



























                                                                                                                                                            
                          
                          


                                                                                                             


































                                                                                                                                                                                   

                                                                                                                                           
                                                                                              
                                                                                                                                           










                                               
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 thiserror::Error;
    use leptos::prelude::*;
    use web_sys::{js_sys::Uint8Array, wasm_bindgen::{prelude::Closure, JsCast, UnwrapThrowExt}, Event, FileReader, HtmlInputElement, ProgressEvent, Url};

    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 || {
            error.with(|error| {
                if let Some(error) = error {
                    view! { <div class="error">{error.to_string()}</div> }.into_any()
                } else {
                    view! {}.into_any()
                }
            })
        };

        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>();
        
            // 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<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();
            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 _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">
                <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>
                    <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>
                    <hr />
                    <input disabled=profile_save_pending class="button" type="submit" value="Save Changes" />
                </form>
            </div>
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SettingsPage {
    Account,
    Chat,
    Profile,
    Privacy,
}

#[component]
pub fn Settings() -> impl IntoView {
    let show_settings: RwSignal<Option<SettingsPage>> = use_context().unwrap();

    view! {
        <Modal on_background_click=move |_| { show_settings.set(None); }>
            <div class="settings panel">
                <div class="header">
                    <h2>Settings</h2>
                    <div class="header-icon close">
                    <IconComponent icon=Icon::Close24 on:click=move |_| show_settings.set(None)/>
                    </div>
                </div>
                <div class="settings-main">
                    <div class="settings-sidebar">
                        <div class:open=move || *show_settings.read() == Some(SettingsPage::Account) on:click=move |_| show_settings.set(Some(SettingsPage::Account))>Account</div>
                        <div class:open=move || *show_settings.read() == Some(SettingsPage::Chat) on:click=move |_| show_settings.set(Some(SettingsPage::Chat))>Chat</div>
                        <div class:open=move || *show_settings.read() == Some(SettingsPage::Privacy) on:click=move |_| show_settings.set(Some(SettingsPage::Privacy))>Privacy</div>
                        <div class:open=move || *show_settings.read() == Some(SettingsPage::Profile) on:click=move |_| show_settings.set(Some(SettingsPage::Profile))>Profile</div>
                    </div>
                    <div class="settings-page">
                        {move || if let Some(page) = show_settings.get() {
                            match page {
                            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 style="padding: 16px">"Privacy settings coming soon!"</div> }.into_any(),
                            }
                        } else {
                            view! {}.into_any()
                        }}
                    </div>
                </div>
            </div>
        </Modal>
    }
}