diff options
| author | 2025-05-15 19:32:01 +0100 | |
|---|---|---|
| committer | 2025-05-15 19:32:01 +0100 | |
| commit | 227661fb8339743f87fc36ca3be1f935050db2d4 (patch) | |
| tree | d1efc7d406595c0daf76d8b0997d24311cac3167 | |
| parent | 62aaa8cb8583d9189358a6c15ca69257b342c4ea (diff) | |
| download | macaw-web-227661fb8339743f87fc36ca3be1f935050db2d4.tar.gz macaw-web-227661fb8339743f87fc36ca3be1f935050db2d4.tar.bz2 macaw-web-227661fb8339743f87fc36ca3be1f935050db2d4.zip  | |
feat: dock user menu
| -rw-r--r-- | assets/style.scss | 69 | ||||
| -rw-r--r-- | src/lib.rs | 167 | 
2 files changed, 233 insertions, 3 deletions
diff --git a/assets/style.scss b/assets/style.scss index 75b058f..b32e35c 100644 --- a/assets/style.scss +++ b/assets/style.scss @@ -513,6 +513,75 @@ p {    }  } +.overlay { +  position: relative; +  z-index: 100; +} + +.overlay-background { +  position: fixed; +  top: 0; +  left: 0; +  width: 100vw; +  height: 100vh; +} + +.overlay-content { +  z-index: 101; +  position: absolute; +} + +.personal .overlay-content { +  bottom: 0; +  left: 100%; +} + +.menu { +  border: 2px solid black; +  background-color: #dcdcdc; +  color: #000000; +  width: 300px; +} + +.menu-item { +  padding: 4px 8px; +} + +.menu-item:hover { +  background-color: rgba(0, 0, 0, 0.1); +} + +.personal-status-menu .user { +  display: flex; +  gap: 8px; +  padding: 8px; +} + +.personal-status-menu .user .user-info { +  display: flex; +  flex-direction: column; +} + +.personal-status-menu .user .nick { +  font-size: 1.1em; +  font-weight: bold; +} +.personal-status-menu .user .jid { +  font-family: Diolce; +} + +hr { +  margin: 0; +} + +.status-edit { +  padding: 0 8px 8px 8px; +} + +.status-edit select { +  width: 100%; +} +  /* font-families */  /* thai */ @@ -12,9 +12,9 @@ use std::{  };  use base64::{Engine, prelude::BASE64_STANDARD}; -use chrono::{NaiveDateTime, TimeDelta}; +use chrono::{NaiveDateTime, TimeDelta, Utc};  use filamento::{ -    chat::{Body, Chat, ChatStoreFields, Delivery, Message, MessageStoreFields}, db::Db, error::{CommandError, ConnectionError, DatabaseError}, files::{opfs::OPFSError, FileStore, FilesMem, FilesOPFS}, presence::{Presence, PresenceType, Show}, roster::{Contact, ContactStoreFields}, user::{User, UserStoreFields}, UpdateMessage +    chat::{Body, Chat, ChatStoreFields, Delivery, Message, MessageStoreFields}, db::Db, error::{CommandError, ConnectionError, DatabaseError}, files::{opfs::OPFSError, FileStore, FilesMem, FilesOPFS}, presence::{Offline, Online, Presence, PresenceType, Show}, roster::{Contact, ContactStoreFields}, user::{User, UserStoreFields}, UpdateMessage  };  use futures::stream::StreamExt;  use indexmap::IndexMap; @@ -46,6 +46,7 @@ pub enum AppState {  #[derive(Clone)]  pub struct Client {      client: filamento::Client<Files>, +    resource: ArcRwSignal<Option<String>>,      jid: Arc<JID>,      file_store: Files,  } @@ -224,15 +225,19 @@ fn LoginPage(                  files.clone(),              );              // TODO: remember_me +            let resource = ArcRwSignal::new(None::<String>);              let client = Client {                  client, +                resource: resource.clone(),                  jid: Arc::new(jid),                  file_store: files,              };              if *connect_on_login.read_untracked() {                  match client.connect().await { -                    Ok(_) => {} +                    Ok(r) => { +                        resource.set(Some(r)) +                    }                      Err(e) => {                          set_error.set(Some(e.into()));                          set_login_pending.set(false); @@ -591,6 +596,17 @@ impl Presences {              presence,          );      } + +    pub fn resource_presence(&mut self, resource: String) -> Presence { +        if let Some(presence) = self.presences.get(&resource) { +            presence.clone() +        } else { +            Presence { +                timestamp: Utc::now(), +                presence: PresenceType::Offline(Offline::default()), +            } +        } +    }  }  #[component] @@ -851,11 +867,24 @@ pub fn PersonalStatus() -> impl IntoView {          let user: Store<User> = <ArcStore<filamento::user::User> as Clone>::clone(&(*user.user)).into();          view! {              <div class="dock-item" class:focused=move || *open.read()  on:click=move |_| { +                debug!("set open to true");                  set_open.update(|state| *state = !*state)              }>                  <AvatarWithPresence user=user />                  <div class="dock-pill"></div>              </div> +            {move || { +                let open = open.get(); +                debug!("open = {:?}", open); +                if open { +                view! { +                    <Overlay set_open> +                        <PersonalStatusMenu user /> +                    </Overlay> +                }.into_any() +            } else { +                view! {}.into_any() +            }}}          }.into_any()      } else {          view! {}.into_any() @@ -863,6 +892,138 @@ pub fn PersonalStatus() -> impl IntoView {  }  #[component] +pub fn PersonalStatusMenu(user: Store<User>) -> impl IntoView { +    let user_presences: Store<UserPresences> = use_context().expect("no user presence store"); +     +    let client = use_context::<Client>().expect("client not in context"); +    let (show_value, set_show_value) = signal({ +        let show = match user_presences.write().get_user_presences(&user.jid().read().as_bare()).write().resource_presence(client.resource.read().clone().unwrap_or_default()).presence { +        PresenceType::Online(online) => match online.show { +            Some(s) => match s { +                Show::Away => 3, +                Show::Chat => 0, +                Show::DoNotDisturb => 2, +                Show::ExtendedAway => 4, +            }, +            None => 1, +        }, +        PresenceType::Offline(_offline) => 5, +    }; +    debug!("initial show = {show}"); +    show +    }); + +    let show_select: NodeRef<html::Select> = NodeRef::new(); + +    let set_status = Action::new_local(move |show_value: &i32| { +        let show_value = show_value.to_owned();     +        let client = client.clone(); +        async move { +            if let Err(e) = match show_value { +                0 => { +                    if let Ok(r) = client.connect().await { +                        client.resource.set(Some(r)) +                    }; +                    client.set_status(Online { show: Some(Show::Chat), ..Default::default() }).await +                }, +                1 => { +                    if let Ok(r) = client.connect().await { +                        client.resource.set(Some(r)) +                    }; +                    client.set_status(Online { show: None, ..Default::default() }).await +                }, +                2 => { +                    if let Ok(r) = client.connect().await { +                        client.resource.set(Some(r)) +                    }; +                    client.set_status(Online { show: Some(Show::DoNotDisturb), ..Default::default() }).await +                }, +                3 => { +                    if let Ok(r) = client.connect().await { +                        client.resource.set(Some(r)) +                    }; +                    client.set_status(Online { show: Some(Show::Away), ..Default::default() }).await +                }, +                4 => { +                    if let Ok(r) = client.connect().await { +                        client.resource.set(Some(r)) +                    }; +                    client.set_status(Online { show: Some(Show::ExtendedAway), ..Default::default() }).await +                }, +                5 => { +                    if let Ok(_) = client.disconnect(Offline::default()).await { +                        client.resource.set(None) +                    } +                    set_show_value.set(5); +                    return +                } +                _ => { +                    error!("invalid availability select"); +                    return +                } +            } { +                error!("show set error: {e}"); +                return +            } +            set_show_value.set(show_value); +        } +    }); + +    view! { +        <div class="personal-status-menu menu"> +            <div class="user"> +                <AvatarWithPresence user=user /> +                <div class="user-info"> +                    <div class="nick">{move || get_name(user)}</div> +                    <div class="jid">{move || user.jid().with(|jid| jid.to_string())}</div> +                </div> +            </div> +            <div class="status-edit"> +                <select +                    node_ref=show_select +                    on:change:target=move |ev| { +                        let show_value = ev.target().value().parse().unwrap(); +                        set_status.dispatch(show_value); +                    } +                    prop:show_value=move || show_value.get().to_string() +                > +                    <option value="0" selected=move || show_value.get_untracked() == 0>Available to Chat</option> +                    <option value="1" selected=move || show_value.get_untracked() == 1>Online</option> +                    <option value="2" selected=move || show_value.get_untracked() == 2>Do not disturb</option> +                    <option value="3" selected=move || show_value.get_untracked() == 3>Away</option> +                    <option value="4" selected=move || show_value.get_untracked() == 4>Extended Away</option> +                    <option value="5" selected=move || show_value.get_untracked() == 5>Offline</option> +                </select> +            </div> +            <hr /> +            <div class="menu-item"> +                Profile +            </div> +            <div class="menu-item"> +                Settings +            </div> +            <hr /> +            <div class="menu-item"> +                Log out +            </div> +        </div> +    } +} + +#[component] +pub fn Overlay(set_open: WriteSignal<bool>, children: Children) -> impl IntoView { +    view! { +        <div class="overlay"> +            <div class="overlay-background" on:click=move |_| { +                debug!("set open to false"); +                set_open.update(|state| *state = false) +            }></div> +            <div class="overlay-content">{children()}</div> +        </div> +    } +} + +#[component]  pub fn OpenChatsPanelView() -> impl IntoView {      let open_chats: Store<OpenChatsPanel> = use_context().expect("no open chats panel in context");  | 
