diff options
| author | 2025-06-01 16:10:26 +0100 | |
|---|---|---|
| committer | 2025-06-01 17:27:40 +0100 | |
| commit | 6ee4190a26f32bfa953302ee363ad3bb6c384ebb (patch) | |
| tree | 2c3182c29d5780a0ad9c9770b5e546312bea49b4 /src/components/roster_list | |
| 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/components/roster_list')
| -rw-r--r-- | src/components/roster_list/contact_request_manager.rs | 198 | ||||
| -rw-r--r-- | src/components/roster_list/roster_list_item.rs | 62 | 
2 files changed, 260 insertions, 0 deletions
diff --git a/src/components/roster_list/contact_request_manager.rs b/src/components/roster_list/contact_request_manager.rs new file mode 100644 index 0000000..174e677 --- /dev/null +++ b/src/components/roster_list/contact_request_manager.rs @@ -0,0 +1,198 @@ +use std::{collections::HashSet, str::FromStr}; + +use filamento::{error::{CommandError, SubscribeError}, roster::ContactStoreFields}; +use jid::{BareJID, JID}; +use leptos::{html::Input, prelude::*}; +use reactive_stores::Store; +use thiserror::Error; + +use crate::{client::Client, roster::{Roster, RosterStoreFields}}; + +#[derive(Clone, Debug, Error)] +pub enum AddContactError { +    #[error("Missing JID")] +    MissingJID, +    #[error("Invalid JID: {0}")] +    InvalidJID(#[from] jid::ParseError), +    #[error("Subscription: {0}")] +    Db(#[from] CommandError<SubscribeError>), +} + +#[component] +// TODO: rename +pub fn AddContact() -> impl IntoView { +    let requests: ReadSignal<HashSet<BareJID>> = use_context().expect("no pending subscriptions in context"); +    let set_requests: WriteSignal<HashSet<BareJID>> = use_context().expect("no pending subscriptions write signal in context"); +    let roster: Store<Roster>  = use_context().expect("no roster in context"); + +    let jid = RwSignal::new("".to_string()); +    // TODO: compartmentalise into error component, form component... +    let (error, set_error) = signal(None::<AddContactError>); +    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 (add_contact_pending, set_add_contact_pending) = signal(false); + +    let client = use_context::<Client>().expect("client not in context"); +    let client2 = client.clone(); +    let client3 = client.clone(); +    let client4 = client.clone(); + +    let add_contact= Action::new_local(move |_| { +        let client = client.clone(); +        async move { +            set_add_contact_pending.set(true); + +            if jid.read_untracked().is_empty() { +                set_error.set(Some(AddContactError::MissingJID)); +                set_add_contact_pending.set(false); +                return; +            } + +            let jid = match JID::from_str(&jid.read_untracked()) { +                Ok(j) => j.to_bare(), +                Err(e) => { +                    set_error.set(Some(e.into())); +                    set_add_contact_pending.set(false); +                    return; +                } +            }; + +            let chat_jid = jid; +            // TODO: more options? +            match client.buddy_request(chat_jid).await { +                Ok(c) => c, +                Err(e) => { +                    set_error.set(Some(e.into())); +                    set_add_contact_pending.set(false); +                   return; +                }, +            }; + +            set_add_contact_pending.set(false); +        } +    }); + +    let jid_input = NodeRef::<Input>::new(); +    let _focus = Effect::new(move |_| { +        if let Some(input) = jid_input.get() { +            let _ = input.focus(); +            input.set_text_content(Some("")); +            // input.style("height: 0"); +            // let height = input.scroll_height(); +            // input.style(format!("height: {}px", height)); +        } +    }); + +    let outgoing = move || roster.contacts().get().into_iter().filter(|(jid, contact)| { +        match *contact.contact.subscription().read() { +            filamento::roster::Subscription::None => false, +            filamento::roster::Subscription::PendingOut => true, +            filamento::roster::Subscription::PendingIn => false, +            filamento::roster::Subscription::PendingInPendingOut => true, +            filamento::roster::Subscription::OnlyOut => false, +            filamento::roster::Subscription::OnlyIn => false, +            filamento::roster::Subscription::OutPendingIn => false, +            filamento::roster::Subscription::InPendingOut => true, +            filamento::roster::Subscription::Buddy => false, +        } +    }).collect::<Vec<_>>(); + +    let accept_friend_request = Action::new_local(move |jid: &BareJID| { +        let client = client2.clone(); +        let jid = jid.clone(); +        async move { +            // TODO: error +            client.accept_buddy_request(jid).await; +        } +    }); + +    let reject_friend_request = Action::new_local(move |jid: &BareJID| { +        let client = client3.clone(); +        let jid = jid.clone(); +        async move { +            // TODO: error +            client.unsubscribe_contact(jid.clone()).await; +            set_requests.write().remove(&jid); +        } +    }); + +    let cancel_subscription_request = Action::new_local(move |jid: &BareJID| { +        let client = client4.clone(); +        let jid = jid.clone(); +        async move { +            // TODO: error +            client.unsubscribe_from_contact(jid).await; + +        } +    }); + +    view! { +        <div class="add-contact-menu"> +        <div> +            {error_message} +            <form on:submit=move |ev| { +                ev.prevent_default(); +                add_contact.dispatch(()); +            }> +                <input +                    disabled=add_contact_pending +                    placeholder="JID" +                    type="text" +                    node_ref=jid_input +                    bind:value=jid +                    name="jid" +                    id="jid" +                    autofocus="true" +                /> +                <input disabled=add_contact_pending class="button" type="submit" value="Send Friend Request" /> +            </form> +        </div> +        {move || if !requests.read().is_empty() { +            view! { +                <div> +                    <h3>Incoming Subscription Requests</h3> +                    <For each=move || requests.get() key=|request| request.clone() let(request)> +                        { +                            let request2 = request.clone(); +                            let request3 = request.clone(); +                            let jid_string = move || request.to_string(); +                            view! { +                            <div class="jid-with-button"><div class="jid">{jid_string}</div> +                            <div><div class="button" on:click=move |_| { accept_friend_request.dispatch(request2.clone()); } >Accept</div><div class="button" on:click=move |_| { reject_friend_request.dispatch(request3.clone()); } >Reject</div></div></div> +                            } +                        } +                    </For> +                </div> +            }.into_any() +        } else { +            view! {}.into_any() +        }} +        {move || if !outgoing().is_empty() { +            view! { +                <div> +                    <h3>Pending Outgoing Subscription Requests</h3> +                    <For each=move || outgoing() key=|(jid, _contact)| jid.clone() let((jid, contact))> +                        { +                            let jid2 = jid.clone(); +                            let jid_string = move || jid.to_string(); +                            view! { +                            <div class="jid-with-button"><div class="jid">{jid_string}</div><div class="button" on:click=move |_| { cancel_subscription_request.dispatch(jid2.clone()); } >Cancel</div></div> +                            } +                        } +                    </For>  +                </div> +            }.into_any() +        } else { +            view! {}.into_any() +        }} +        </div> +    } +} + diff --git a/src/components/roster_list/roster_list_item.rs b/src/components/roster_list/roster_list_item.rs new file mode 100644 index 0000000..46ac1cc --- /dev/null +++ b/src/components/roster_list/roster_list_item.rs @@ -0,0 +1,62 @@ +use std::ops::Deref; + +use filamento::{chat::Chat, roster::{Contact, ContactStoreFields}, user::{User, UserStoreFields}}; +use leptos::prelude::*; +use reactive_stores::{ArcStore, Store}; +use tracing::debug; + +use crate::{chat::MacawChat, components::{avatar::AvatarWithPresence, sidebar::Open}, contact::MacawContact, open_chats::{OpenChatsPanel, OpenChatsPanelStoreFields}, user::get_name}; + +#[component] +pub fn RosterListItem(contact: MacawContact) -> impl IntoView { +    let contact_contact: Store<Contact> = contact.contact; +    let contact_user: Store<User> = +        <ArcStore<filamento::user::User> as Clone>::clone(&contact.user).into(); +    let name = move || get_name(contact_user, false); + +    let open_chats: Store<OpenChatsPanel> = +        use_context().expect("no open chats panel store in context"); + +    // TODO: why can this not be in the closure????? +    // TODO: not good, as overwrites preexisting chat state with possibly incorrect one... +    let chat = Chat { +        correspondent: contact_user.jid().get(), +        have_chatted: false, +    }; +    let chat = MacawChat::got_chat_and_user(chat, contact_user.get()); + +    let open_chat = move |_| { +        debug!("opening chat"); +        open_chats.update(|open_chats| open_chats.open(chat.clone())); +    }; + +    let open = move || { +        if let Some(open_chat) = &*open_chats.chat_view().read() { +            debug!("got open chat: {:?}", open_chat); +            if *open_chat == *contact_user.jid().read() { +                return Open::Focused; +            } +        } +        if let Some(_backgrounded_chat) = open_chats +            .chats() +            .read() +            .get(contact_user.jid().read().deref()) +        { +            return Open::Open; +        } +        Open::Closed +    }; +    let focused = move || open().is_focused(); +    let open = move || open().is_open(); + +    view! { +        <div class="roster-list-item" class:open=move || open() class:focused=move || focused() on:click=open_chat> +            <AvatarWithPresence user=contact_user /> +            <div class="item-info"> +                <div class="main-info"><p class="name">{name}<span class="jid"> - {move || contact_contact.user_jid().read().to_string()}</span></p></div> +                <div class="sub-info">{move || contact_contact.subscription().read().to_string()}</div> +            </div> +        </div> +    } +} +  | 
