summaryrefslogtreecommitdiffstats
path: root/src/components/sidebar.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/components/sidebar.rs233
1 files changed, 233 insertions, 0 deletions
diff --git a/src/components/sidebar.rs b/src/components/sidebar.rs
new file mode 100644
index 0000000..9f555b5
--- /dev/null
+++ b/src/components/sidebar.rs
@@ -0,0 +1,233 @@
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::collections::{HashMap, HashSet};
+
+use jid::BareJID;
+use leptos::prelude::*;
+use reactive_stores::Store;
+
+use crate::components::{
+ chats_list::ChatsList, personal_status::PersonalStatus, roster_list::RosterList,
+};
+
+#[derive(PartialEq, Eq, Clone, Copy, Hash)]
+pub enum SidebarOpen {
+ Roster,
+ Chats,
+}
+
+#[derive(Store)]
+pub struct Drawer {
+ open: SidebarOpen,
+ hovering: bool,
+}
+
+pub enum Open {
+ /// Currently on screen
+ Focused,
+ /// Open in background somewhere (e.g. in another chat tab)
+ Open,
+ /// Closed
+ Closed,
+}
+
+impl Open {
+ pub fn is_focused(&self) -> bool {
+ match self {
+ Open::Focused => true,
+ Open::Open => false,
+ Open::Closed => false,
+ }
+ }
+
+ pub fn is_open(&self) -> bool {
+ match self {
+ Open::Focused => true,
+ Open::Open => true,
+ Open::Closed => false,
+ }
+ }
+}
+
+/// 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;
+ false
+ } else {
+ *state = Some(open);
+ true
+ }
+ }
+ None => {
+ *state = Some(open);
+ true
+ }
+ }
+}
+
+#[component]
+pub fn Sidebar() -> impl IntoView {
+ let requests: ReadSignal<HashSet<BareJID>> =
+ use_context().expect("no pending subscriptions in context");
+
+ // for what has been clicked open (in the background)
+ let (open, set_open) = signal(None::<SidebarOpen>);
+ // for what is just in the hovered state (not clicked to be pinned open yet necessarily)
+ let open = Memo::new(move |_| open.get());
+ let (hovered, set_hovered) = signal(None::<SidebarOpen>);
+ let hovered = Memo::new(move |_| hovered.get());
+ let (just_closed, set_just_closed) = signal(false);
+ let just_closed = Memo::new(move |_| just_closed.get());
+
+ let pages = Memo::new(move |_| {
+ let mut pages = HashSet::new();
+ if let Some(hovered) = *hovered.read() {
+ pages.insert(hovered);
+ }
+ if let Some(opened) = *open.read() {
+ pages.insert(opened);
+ }
+ pages
+ });
+
+ view! {
+ <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::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);
+ } else {
+ set_just_closed.set(false);
+ }
+ })
+ }
+ >
+ <div class="dock-pill"></div>
+ <div class="dock-icon">
+ <div class="icon-with-badge">
+ <img src="/assets/caw.png" />
+ {move || {
+ let len = requests.read().len();
+ if len > 0 {
+ view! { <div class="badge">{len}</div> }.into_any()
+ } else {
+ view! {}.into_any()
+ }
+ }}
+ </div>
+ </div>
+ </div>
+ <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);
+ } else {
+ set_just_closed.set(false);
+ }
+ })
+ }
+ >
+ <div class="dock-pill"></div>
+ <img src="/assets/bubble.png" />
+ </div>
+ </div>
+ <div class="pins"></div>
+ <div class="personal">
+ <PersonalStatus />
+ </div>
+ </div>
+ {move || {
+ if !just_closed.get() {
+ view! {
+ <For each=move || pages.get() key=|page| *page let(page)>
+ {move || match page {
+ SidebarOpen::Roster => {
+ view! {
+ {move || {
+ if *open.read() == None
+ && *hovered.read() == Some(SidebarOpen::Roster)
+ {
+ view! { <div class="sidebar-drawer behind-hovering"></div> }
+ .into_any()
+ } else {
+ view! {}.into_any()
+ }
+ }}
+ <div
+ class:sidebar-drawer=true
+ class:sidebar-hovering-drawer=move || {
+ !(*open.read() == Some(SidebarOpen::Roster))
+ && (*hovered.read() == Some(SidebarOpen::Roster))
+ }
+ >
+ <RosterList />
+ </div>
+ }
+ .into_any()
+ }
+ SidebarOpen::Chats => {
+ view! {
+ {move || {
+ if *open.read() == None
+ && *hovered.read() == Some(SidebarOpen::Chats)
+ {
+ view! { <div class="sidebar-drawer behind-hovering"></div> }
+ .into_any()
+ } else {
+ view! {}.into_any()
+ }
+ }}
+ <div
+ class:sidebar-drawer=true
+ class:sidebar-hovering-drawer=move || {
+ !(*open.read() == Some(SidebarOpen::Chats))
+ && (*hovered.read() == Some(SidebarOpen::Chats))
+ }
+ >
+ <ChatsList />
+ </div>
+ }
+ .into_any()
+ }
+ }}
+ </For>
+ }
+ .into_any()
+ } else {
+ view! {}.into_any()
+ }
+ }}
+ </div>
+ }
+}