diff options
| author | 2025-06-01 16:10:26 +0100 | |
|---|---|---|
| committer | 2025-06-01 17:27:40 +0100 | |
| commit | 6ee4190a26f32bfa953302ee363ad3bb6c384ebb (patch) | |
| tree | 2c3182c29d5780a0ad9c9770b5e546312bea49b4 /src/views/macaw | |
| parent | f76c80c1d23177ab00c81240ee3a75d3bcda0e3b (diff) | |
| download | macaw-web-6ee4190a26f32bfa953302ee363ad3bb6c384ebb.tar.gz macaw-web-6ee4190a26f32bfa953302ee363ad3bb6c384ebb.tar.bz2 macaw-web-6ee4190a26f32bfa953302ee363ad3bb6c384ebb.zip  | |
refactor: reorganise code
Diffstat (limited to 'src/views/macaw')
| -rw-r--r-- | src/views/macaw/open_chats_panel.rs | 73 | ||||
| -rw-r--r-- | src/views/macaw/settings.rs | 170 | 
2 files changed, 243 insertions, 0 deletions
diff --git a/src/views/macaw/open_chats_panel.rs b/src/views/macaw/open_chats_panel.rs new file mode 100644 index 0000000..062c786 --- /dev/null +++ b/src/views/macaw/open_chats_panel.rs @@ -0,0 +1,73 @@ +use leptos::prelude::*; +use open_chat::OpenChatView; +use reactive_stores::{ArcStore, Store}; + +use crate::open_chats::{OpenChatsPanel, OpenChatsPanelStoreFields}; + +// TODO: multiple panels +// pub struct OpenChats { +//     panels: +// } + +#[component] +pub fn OpenChatsPanelView() -> impl IntoView { +    let open_chats: Store<OpenChatsPanel> = use_context().expect("no open chats panel in context"); + +    // TODO: tabs +    // view! { +    //     {move || { +    //         if open_chats.chats().read().len() > 1 { +    //             Some( +    //                 view! { +    //                     <For +    //                         each=move || open_chats.chats().get() +    //                         key=|(jid, _)| jid.clone() +    //                         let(chat) +    //                     ></For> +    //                 }, +    //             ) +    //         } else { +    //             None +    //         } +    //     }} +    // } +    view! { +        <div class="open-chat-views"> +            {move || { +                if let Some(open_chat) = open_chats.chat_view().get() { +                    if let Some(open_chat) = open_chats.chats().read().get(&open_chat) { +                        view! { <OpenChatView chat=open_chat.clone() /> }.into_any() +                    } else { +                        view! {}.into_any() +                    } +                } else { +                    view! {}.into_any() +                } +            }} +        </div> +    } +} + +mod open_chat { +    use filamento::chat::{Chat, ChatStoreFields}; +    use leptos::prelude::*; +    use reactive_stores::{ArcStore, Store}; + +    use crate::{chat::MacawChat, components::{chat_header::ChatViewHeader, message_composer::ChatViewMessageComposer, message_history_buffer::MessageHistoryBuffer}}; + +    #[component] +    pub fn OpenChatView(chat: MacawChat) -> impl IntoView { +        let chat_chat: Store<Chat> = +            <ArcStore<filamento::chat::Chat> as Clone>::clone(&chat.chat).into(); +        let chat_jid = move || chat_chat.correspondent().get(); + +        view! { +            <div class="open-chat-view"> +                <ChatViewHeader chat=chat.clone() /> +                <MessageHistoryBuffer chat=chat.clone() /> +                <ChatViewMessageComposer chat=chat_jid() /> +            </div> +        } +    } +} + diff --git a/src/views/macaw/settings.rs b/src/views/macaw/settings.rs new file mode 100644 index 0000000..11a3fc3 --- /dev/null +++ b/src/views/macaw/settings.rs @@ -0,0 +1,170 @@ +use leptos::prelude::*; +use profile_settings::ProfileSettings; + +use crate::{components::{icon::IconComponent, modal::Modal}, icon::Icon}; + +mod profile_settings { +    use filamento::error::{AvatarPublishError, CommandError}; +    use thiserror::Error; +    use leptos::prelude::*; +    use web_sys::{js_sys::Uint8Array, wasm_bindgen::{prelude::Closure, JsCast, UnwrapThrowExt}, Event, FileReader, HtmlInputElement, ProgressEvent}; + +    use crate::{client::Client, files::Files}; + +    #[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! { +            <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> +        } +    } +} + +#[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>"account"</div> }.into_any(), +                            SettingsPage::Chat => view! { <div>"chat"</div> }.into_any(), +                            SettingsPage::Profile => view! { <ProfileSettings /> }.into_any(), +                            SettingsPage::Privacy => view! { <div>"privacy"</div> }.into_any(), +                            } +                        } else { +                            view! {}.into_any() +                        }} +                    </div> +                </div> +            </div> +        </Modal> +    } +} +  | 
