summaryrefslogblamecommitdiffstats
path: root/src/views/macaw/settings.rs
blob: 1a23b8238595c54871884eb31ec6a4da1f0231c2 (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 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<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_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! {
            <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>
    }
}