diff options
author | 2025-05-10 02:31:40 +0100 | |
---|---|---|
committer | 2025-05-10 02:31:40 +0100 | |
commit | cf2ffc5ecb2eefbb425a4d05583d3e4d9645bf2b (patch) | |
tree | ad945d8311750cb773d289ddfacf0dd67b962304 /src/lib.rs | |
parent | edd38ee4a7164170e9e456a92be286609062424c (diff) | |
download | macaw-web-cf2ffc5ecb2eefbb425a4d05583d3e4d9645bf2b.tar.gz macaw-web-cf2ffc5ecb2eefbb425a4d05583d3e4d9645bf2b.tar.bz2 macaw-web-cf2ffc5ecb2eefbb425a4d05583d3e4d9645bf2b.zip |
feat: presences and presence icons
Diffstat (limited to 'src/lib.rs')
-rw-r--r-- | src/lib.rs | 217 |
1 files changed, 191 insertions, 26 deletions
@@ -14,16 +14,7 @@ use std::{ use base64::{Engine, prelude::BASE64_STANDARD}; use chrono::{NaiveDateTime, TimeDelta}; use filamento::{ - UpdateMessage, - chat::{Body, Chat, ChatStoreFields, Delivery, Message, MessageStoreFields}, - db::Db, - error::{CommandError, ConnectionError, DatabaseError}, - files::FileStore, - files::FilesMem, - files::FilesOPFS, - files::opfs::OPFSError, - roster::{Contact, ContactStoreFields}, - user::{User, UserStoreFields}, + 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 }; use futures::stream::StreamExt; use indexmap::IndexMap; @@ -204,8 +195,9 @@ fn LoginPage( } }; + let remember_me = remember_me.get_untracked(); // initialise the client - let db = if remember_me.get_untracked() { + let db = if remember_me { debug!("creating db in opfs"); Db::create_connect_and_migrate(jid.as_bare().to_string()) .await @@ -214,7 +206,7 @@ fn LoginPage( debug!("creating db in memory"); Db::create_connect_and_migrate_memory().await.unwrap() }; - let files = if remember_me.get_untracked() { + let files = if remember_me { let opfs = FilesOPFS::new(jid.as_bare().to_string()).await; match opfs { Ok(f) => Files::Opfs(f), @@ -490,6 +482,119 @@ impl OpenChatsPanel { // } } +#[derive(Store)] +pub struct UserPresences { + #[store(key: JID = |(jid, _)| jid.clone())] + user_presences: HashMap<JID, RwSignal<Presences>>, +} + +impl UserPresences { + pub fn clear(&mut self) { + for (_user, presences) in &mut self.user_presences { + presences.set(Presences::new()) + } + } + + // TODO: should be a bare jid + pub fn get_user_presences(&mut self, user: &JID) -> RwSignal<Presences> { + if let Some(presences) = self.user_presences.get(user) { + *presences + } else { + let presences = Presences::new(); + let signal = RwSignal::new(presences); + self.user_presences.insert(user.clone(), signal); + signal + } + } +} + +impl UserPresences { + pub fn new() -> Self { + Self { + user_presences: HashMap::new(), + } + } +} + +pub struct Presences { + /// presences are sorted by time, first by type, then by last activity. + presences: IndexMap<String, Presence> +} + +impl Presences { + pub fn new() -> Self { + Self { + presences: IndexMap::new(), + } + } + + /// gets the highest priority presence + pub fn presence(&self) -> Option<(String, Presence)> { + if let Some((resource, presence)) = self.presences.iter().filter(|(_resource, presence)| if let PresenceType::Online(online) = &presence.presence { + online.show == Some(Show::DoNotDisturb) + } else { + false + }).next() { + return Some((resource.clone(), presence.clone())) + } + if let Some((resource, presence)) = self.presences.iter().filter(|(_resource, presence)| if let PresenceType::Online(online) = &presence.presence { + online.show == Some(Show::Chat) + } else { + false + }).next() { + return Some((resource.clone(), presence.clone())) + } + if let Some((resource, presence)) = self.presences.iter().filter(|(_resource, presence)| if let PresenceType::Online(online) = &presence.presence { + online.show == None + } else { + false + }).next() { + return Some((resource.clone(), presence.clone())) + } + if let Some((resource, presence)) = self.presences.iter().filter(|(_resource, presence)| if let PresenceType::Online(online) = &presence.presence { + online.show == Some(Show::Away) + } else { + false + }).next() { + return Some((resource.clone(), presence.clone())) + } + if let Some((resource, presence)) = self.presences.iter().filter(|(_resource, presence)| if let PresenceType::Online(online) = &presence.presence { + online.show == Some(Show::ExtendedAway) + } else { + false + }).next() { + return Some((resource.clone(), presence.clone())) + } + if let Some((resource, presence)) = self.presences.iter().filter(|(_resource, presence)| if let PresenceType::Offline(_offline) = &presence.presence { + true + } else { + false + }).next() { + return Some((resource.clone(), presence.clone())) + } else { + None + } + } + + pub fn update_presence(&mut self, resource: String, presence: Presence) { + let index = match self.presences.binary_search_by(|_, existing_presence| { + presence.timestamp + .cmp( + &existing_presence.timestamp + ) + }) { + Ok(i) => i, + Err(i) => i, + }; + self.presences.insert_before( + // TODO: check if this logic is correct + index, + resource, + presence, + ); + } +} + #[component] fn Macaw( // TODO: logout @@ -515,6 +620,9 @@ fn Macaw( let open_chats = Store::new(OpenChatsPanel::default()); provide_context(open_chats); + let user_presences = Store::new(UserPresences::new()); + provide_context(user_presences); + // TODO: get cached contacts on login before getting the updated contacts OnceResource::new(async move { @@ -532,7 +640,10 @@ fn Macaw( .collect(); roster.contacts().set(contacts); } - UpdateMessage::Offline(offline) => {} + UpdateMessage::Offline(offline) => { + // when offline, will no longer receive updated user presences, consider everybody offline. + user_presences.write().clear(); + } UpdateMessage::RosterUpdate(contact, user) => { roster.contacts().update(|roster| { if let Some(macaw_contact) = roster.get_mut(&contact.user_jid) { @@ -549,7 +660,20 @@ fn Macaw( roster.remove(&jid); }); } - UpdateMessage::Presence { from, presence } => {} + UpdateMessage::Presence { from, presence } => { + let bare_jid = from.as_bare(); + if let Some(presences) = user_presences.read().user_presences.get(&bare_jid) { + if let Some(resource) = from.resourcepart { + presences.write().update_presence(resource, presence); + } + } else { + if let Some(resource) = from.resourcepart { + let mut presences = Presences::new(); + presences.update_presence(resource, presence); + user_presences.write().user_presences.insert(bare_jid, RwSignal::new(presences)); + } + } + } UpdateMessage::Message { to, from, message } => { debug!("before got message"); let new_message = MacawMessage::got_message_and_user(message, from); @@ -707,18 +831,56 @@ pub fn OpenChatView(chat: MacawChat) -> impl IntoView { } } +pub fn show_to_icon(show: Show) -> Icon { + match show { + Show::Away => Icon::Away16Color, + Show::Chat => Icon::Chat16Color, + Show::DoNotDisturb => Icon::Dnd16Color, + Show::ExtendedAway => Icon::Xa16Color, + } +} + +#[component] +pub fn AvatarWithPresence(user: Store<User>) -> impl IntoView { + let avatar = LocalResource::new(move || get_avatar(user)); + let user_presences: Store<UserPresences> = use_context().expect("no user presences in context"); + let presence = move || user_presences.write().get_user_presences(&user.read().jid).read().presence(); + let show_icon = move || presence().map(|(_, presence)| { + match presence.presence { + PresenceType::Online(online) => if let Some(show) = online.show { + Some(show_to_icon(show)) + } else { + Some(Icon::Available16Color) + }, + PresenceType::Offline(offline) => None, + } + }).unwrap_or_default(); + + view! { + <Transition fallback=|| view! { <img class="avatar" src=NO_AVATAR /> } > + <div class="avatar-with-presence"> + <img class="avatar" src=move || avatar.read().as_deref().map(|avatar| avatar.clone()).unwrap_or_default() /> + {move || if let Some(icon) = show_icon() { + view!{ + <IconComponent icon=icon class:presence-show-icon=true /> + }.into_any() + } else { + view! {}.into_any() + }} + </div> + </Transition> + } +} + #[component] pub fn ChatViewHeader(chat: MacawChat) -> impl IntoView { let chat_user = <ArcStore<filamento::user::User> as Clone>::clone(&chat.user).into(); - let avatar = LocalResource::new(move || get_avatar(chat_user)); let name = move || get_name(chat_user); let jid = move || chat_user.jid().read().to_string(); view! { <div class="chat-view-header panel"> - <Transition fallback=|| view! { <img class="avatar" src=NO_AVATAR /> } > - <img class="avatar" src=move || avatar.read().as_deref().map(|avatar| avatar.clone()).unwrap_or_default() /> - </Transition> + <AvatarWithPresence user=chat_user /> <div class="user-info"> <h2 class="name">{name}</h2> <h3>{jid}</h3> @@ -911,6 +1073,9 @@ pub enum Icon { Reply24, Sending16, Sent16, + Chat16Color, + Xa16Color, + Available16Color, } pub const ICONS_SRC: &str = "/assets/icons/"; @@ -936,6 +1101,9 @@ impl Icon { Icon::Reply24 => format!("{}reply24.svg", ICONS_SRC), Icon::Sending16 => format!("{}sending16.svg", ICONS_SRC), Icon::Sent16 => format!("{}sent16.svg", ICONS_SRC), + Icon::Chat16Color => format!("{}chat16color.svg", ICONS_SRC), + Icon::Xa16Color => format!("{}xa16color.svg", ICONS_SRC), + Icon::Available16Color => format!("{}available16color.svg", ICONS_SRC), } } @@ -959,6 +1127,9 @@ impl Icon { Icon::Reply24 => 24, Icon::Sending16 => 16, Icon::Sent16 => 16, + Icon::Chat16Color => 16, + Icon::Xa16Color => 16, + Icon::Available16Color => 16, } } } @@ -1588,7 +1759,6 @@ 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 avatar = LocalResource::new(move || get_avatar(contact_user)); let name = move || get_name(contact_user); let open_chats: Store<OpenChatsPanel> = @@ -1628,9 +1798,7 @@ fn RosterListItem(contact: MacawContact) -> impl IntoView { view! { <div class="roster-list-item" class:open=move || open() class:focused=move || focused() on:click=open_chat> - <Transition fallback=|| view! { <img class="avatar" src=NO_AVATAR /> } > - <img class="avatar" src=move || avatar.read().as_deref().map(|avatar| avatar.clone()).unwrap_or_default() /> - </Transition> + <AvatarWithPresence user=contact_user /> <div class="item-info"> <h3>{name}</h3> </div> @@ -1702,7 +1870,6 @@ fn ChatsListItem(chat: MacawChat, message: MacawMessage) -> impl IntoView { let chat_chat: Store<Chat> = <ArcStore<Chat> as Clone>::clone(&chat.chat).into(); let chat_user: Store<User> = <ArcStore<filamento::user::User> as Clone>::clone(&chat.user).into(); - let avatar = LocalResource::new(move || get_avatar(chat_user)); let name = move || get_name(chat_user); // TODO: store fine-grained reactivity @@ -1736,9 +1903,7 @@ fn ChatsListItem(chat: MacawChat, message: MacawMessage) -> impl IntoView { view! { <div class="chats-list-item" class:open=move || open() class:focused=move || focused() on:click=open_chat> - <Transition fallback=|| view! { <img class="avatar" src=NO_AVATAR /> } > - <img class="avatar" src=move || avatar.read().as_deref().map(|avatar| avatar.clone()).unwrap_or_default() /> - </Transition> + <AvatarWithPresence user=chat_user /> <div class="item-info"> <h3>{name}</h3> <p>{latest_message_body}</p> |