diff options
| author | 2025-05-15 09:09:37 +0100 | |
|---|---|---|
| committer | 2025-05-15 09:09:37 +0100 | |
| commit | 62aaa8cb8583d9189358a6c15ca69257b342c4ea (patch) | |
| tree | ab63c80e8d38df4e4ef3c3b8a41bccde82d4ffdd | |
| parent | cf6e64da8dfb78dd103727066fa494d79912fe2a (diff) | |
| download | macaw-web-62aaa8cb8583d9189358a6c15ca69257b342c4ea.tar.gz macaw-web-62aaa8cb8583d9189358a6c15ca69257b342c4ea.tar.bz2 macaw-web-62aaa8cb8583d9189358a6c15ca69257b342c4ea.zip  | |
feat: dock hover and pinning focus
| -rw-r--r-- | assets/style.scss | 54 | ||||
| -rw-r--r-- | src/lib.rs | 124 | 
2 files changed, 159 insertions, 19 deletions
diff --git a/assets/style.scss b/assets/style.scss index b03393c..75b058f 100644 --- a/assets/style.scss +++ b/assets/style.scss @@ -147,7 +147,7 @@ p {    border-width: 0 2px 0 0;    border-color: black;    border-style: solid; -  height: auto; +  height: 100%;    width: 400px;    flex: 0 0 auto;    align-self: stretch; @@ -405,17 +405,16 @@ p {  .sidebar {    display: flex; +  z-index: 1;  }  .dock { +  z-index: 30;    display: flex;    flex-direction: column;    border-width: 0 2px 0 0;  } -.shortcuts { -  padding: 8px 0; -}  .dock-item {    display: flex; @@ -451,11 +450,12 @@ p {    opacity: 100%;  } -.shortcuts { +.dock .shortcuts { +  padding: 8px 0;    border-bottom: 2px solid black;  } -.dock-item img { +.dock .shortcuts .dock-item img {    width: 64px;    height: 64px;    padding: 4px 8px; @@ -471,6 +471,48 @@ p {    right: 0;  } +.dock .pins { +  flex: 1 1 auto; +} + +.dock .personal { +  padding: 8px 0; +  flex: 0 0 auto; +  border-top: 2px solid black; +} + +.dock .personal .dock-item { +  padding: 8px 16px +} + +.dock .personal .avatar-with-presence .avatar { +  width: 48px; +  height: 48px; +} + +.sidebar-drawer { +  height: auto; +  position: relative; +  z-index: 5; +} + +.sidebar-hovering-drawer { +  z-index: 10; +} + +.sidebar .sidebar-hovering-drawer>* { +  position: absolute; +  left: 0; +  transition: left 0.2s ease-in; +  transition-behaviour: allow-discrete; +} +@starting-style { +  .sidebar .sidebar-hovering-drawer>* { +    /* TODO: allow to edit this width */ +    left: -400px; +  } +} +  /* font-families */  /* thai */ @@ -621,6 +621,15 @@ fn Macaw(      let user_presences = Store::new(UserPresences::new());      provide_context(user_presences); +    let client_user = LocalResource::new(move || { +        async move { +            let client = use_context::<Client>().expect("client not in context"); +            let user = client.get_user((*client.jid).clone()).await.unwrap(); +            MacawUser::got_user(user) +        } +    }); +    provide_context(client_user); +      // TODO: get cached contacts on login before getting the updated contacts      OnceResource::new(async move { @@ -717,16 +726,22 @@ pub enum SidebarOpen {      Chats,  } -pub fn toggle_open(state: &mut Option<SidebarOpen>, open: SidebarOpen) { +/// returns whether the state was changed to open (true) or closed (false) +pub fn toggle_open(state: &mut Option<SidebarOpen>, open: SidebarOpen) -> bool {      match state {          Some(opened) => {              if *opened == open { -                *state = None +                *state = None; +                false              } else { -                *state = Some(open) +                *state = Some(open); +                true              }          } -        None => *state = Some(open), +        None => { +          *state = Some(open);   +          true +        },      }  } @@ -736,19 +751,41 @@ pub fn Sidebar() -> impl IntoView {      let (open, set_open) = signal(None::<SidebarOpen>);      // for what is just in the hovered state (not clicked to be pinned open yet necessarily)      let (hovered, set_hovered) = signal(None::<SidebarOpen>); +    let (just_closed, set_just_closed) = signal(false);      view! { -        <div class="sidebar"> +        <div class="sidebar" on:mouseleave=move |_| { +            set_hovered.set(None); +            set_just_closed.set(false); +        }>              <div class="dock panel">                  <div class="shortcuts"> -                    <div class="roster-tab dock-item" class:focused=move || *open.read() == Some(SidebarOpen::Roster) class:hovering=move || *hovered.read() == Some(SidebarOpen::Chats) on:click=move |_| { -                        set_open.update(|state| toggle_open(state, SidebarOpen::Roster)) +                    <div class="roster-tab dock-item" class:focused=move || *open.read() == Some(SidebarOpen::Roster) class:hovering=move || *hovered.read() == Some(SidebarOpen::Roster) +                    on:mouseenter=move |_| { +                        set_just_closed.set(false); +                        set_hovered.set(Some(SidebarOpen::Roster)) +                    } +                    on:click=move |_| { +                        set_open.update(|state| { +                            if !toggle_open(state, SidebarOpen::Roster) { +                                set_just_closed.set(true); +                            }                              +                        })                      }>                          <div class="dock-pill"></div>                          <img src="/assets/caw.png" />                      </div> -                    <div class="chats-tab dock-item" class:focused=move || *open.read() == Some(SidebarOpen::Chats) class:hovering=move || *hovered.read() == Some(SidebarOpen::Chats) on:click=move |_| { -                        set_open.update(|state| toggle_open(state, SidebarOpen::Chats)) +                    <div class="chats-tab dock-item" class:focused=move || *open.read() == Some(SidebarOpen::Chats) class:hovering=move || *hovered.read() == Some(SidebarOpen::Chats) +                    on:mouseenter=move |_| { +                        set_just_closed.set(false); +                        set_hovered.set(Some(SidebarOpen::Chats)) +                    } +                    on:click=move |_| { +                        set_open.update(|state| { +                            if !toggle_open(state, SidebarOpen::Chats) { +                                set_just_closed.set(true); +                            }                              +                        })                      }>                          <div class="dock-pill"></div>                          <img src="/assets/bubble.png" /> @@ -757,15 +794,45 @@ pub fn Sidebar() -> impl IntoView {                  <div class="pins">                  </div>                  <div class="personal"> +                    <PersonalStatus />                  </div>              </div> +            {move || if let Some(hovered) = *hovered.read() { +                if Some(hovered) != *open.read() { +                    if !just_closed.get() { +                        match hovered { +                            SidebarOpen::Roster => view! { +                                <div class="sidebar-drawer sidebar-hovering-drawer"> +                                    <RosterList /> +                                </div> +                            }.into_any(), +                            SidebarOpen::Chats => view! { +                                <div class="sidebar-drawer sidebar-hovering-drawer"> +                                    <ChatsList /> +                                </div> +                            }.into_any(), +                        } +                    } else { +                         +                        view! {}.into_any() +                    } +                } else { +                    view! {}.into_any() +                } +            } else { +                    view! {}.into_any() +            }}              {move || if let Some(opened) = *open.read() {                  match opened {                      SidebarOpen::Roster => view! { -                        <RosterList /> +                        <div class="sidebar-drawer"> +                            <RosterList /> +                        </div>                      }.into_any(),                      SidebarOpen::Chats => view! { -                        <ChatsList /> +                        <div class="sidebar-drawer"> +                            <ChatsList /> +                        </div>                      }.into_any(),                  }              } else { @@ -776,6 +843,26 @@ pub fn Sidebar() -> impl IntoView {  }  #[component] +pub fn PersonalStatus() -> impl IntoView { +    let user: LocalResource<MacawUser> = use_context().expect("no local user in context"); + +    let (open, set_open) = signal(false); +    move || if let Some(user) = user.get() { +        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 |_| { +                set_open.update(|state| *state = !*state) +            }> +                <AvatarWithPresence user=user /> +                <div class="dock-pill"></div> +            </div> +        }.into_any() +    } else { +        view! {}.into_any() +    } +} + +#[component]  pub fn OpenChatsPanelView() -> impl IntoView {      let open_chats: Store<OpenChatsPanel> = use_context().expect("no open chats panel in context"); @@ -1605,12 +1692,23 @@ impl DerefMut for MacawMessage {      }  } +#[derive(Clone)]  struct MacawUser { -    user: StateListener<JID, Store<User>>, +    user: StateListener<JID, ArcStore<User>>, +} + +impl MacawUser { +    fn got_user(user: User) -> Self { +         +        let user_state_store: StateStore<JID, ArcStore<User>> = +            use_context().expect("no user state store"); +        let user = user_state_store.store(user.jid.clone(), ArcStore::new(user)); +        Self { user } +    }  }  impl Deref for MacawUser { -    type Target = StateListener<JID, Store<User>>; +    type Target = StateListener<JID, ArcStore<User>>;      fn deref(&self) -> &Self::Target {          &self.user  | 
