diff options
-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"); |