use std::{ borrow::Borrow, cell::RefCell, collections::{HashMap, HashSet}, marker::PhantomData, ops::{Deref, DerefMut}, rc::Rc, str::FromStr, sync::{atomic::AtomicUsize, Arc, RwLock}, thread::sleep, time::{self, Duration}, }; use base64::{Engine, prelude::BASE64_STANDARD}; use chrono::{Local, NaiveDateTime, TimeDelta, Utc}; use filamento::{ chat::{Body, Chat, ChatStoreFields, Delivery, Message, MessageStoreFields}, db::Db, error::{CommandError, ConnectionError, DatabaseError, SubscribeError}, 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; use jid::{JID, BareJID}; use leptos::{ ev::{Event, KeyboardEvent, MouseEvent, SubmitEvent}, html::{self, Div, Input, Pre, Textarea}, prelude::*, tachys::{dom::document, reactive_graph::bind::GetValue}, task::{spawn, spawn_local}, }; use reactive_stores::{ArcStore, Store, StoreField}; use stylance::import_style; use thiserror::Error; use tokio::sync::{ Mutex, mpsc::{self, Receiver}, }; use tracing::{debug, error}; use uuid::Uuid; const NO_AVATAR: &str = "/assets/no-avatar.png"; pub enum AppState { LoggedOut, LoggedIn, } #[derive(Clone)] pub struct Client { client: filamento::Client, resource: ArcRwSignal>, jid: Arc, file_store: Files, } #[derive(Clone)] pub enum Files { Mem(FilesMem), Opfs(FilesOPFS), } impl FileStore for Files { type Err = OPFSError; async fn is_stored(&self, name: &str) -> Result { match self { Files::Mem(files_mem) => Ok(files_mem.is_stored(name).await.unwrap()), Files::Opfs(files_opfs) => Ok(files_opfs.is_stored(name).await?), } } async fn store(&self, name: &str, data: &[u8]) -> Result<(), Self::Err> { match self { Files::Mem(files_mem) => Ok(files_mem.store(name, data).await.unwrap()), Files::Opfs(files_opfs) => Ok(files_opfs.store(name, data).await?), } } async fn delete(&self, name: &str) -> Result<(), Self::Err> { match self { Files::Mem(files_mem) => Ok(files_mem.delete(name).await.unwrap()), Files::Opfs(files_opfs) => Ok(files_opfs.delete(name).await?), } } } impl Files { pub async fn get_src(&self, file_name: &str) -> Option { match self { Files::Mem(files_mem) => { if let Some(data) = files_mem.get_file(file_name).await { let data = BASE64_STANDARD.encode(data); Some(format!("data:image/jpg;base64, {}", data)) } else { None } } Files::Opfs(files_opfs) => files_opfs.get_src(file_name).await.ok(), } } } impl Deref for Client { type Target = filamento::Client; fn deref(&self) -> &Self::Target { &self.client } } impl DerefMut for Client { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.client } } #[component] pub fn App() -> impl IntoView { let (app, set_app) = signal(AppState::LoggedOut); let (client, set_client) = signal(None::<(Client, Receiver)>); view! { {move || match &*app.read() { AppState::LoggedOut => view! { }.into_any(), AppState::LoggedIn => { if let Some((client, updates)) = set_client.write_untracked().take() { view! { }.into_any() } else { set_app.set(AppState::LoggedOut); view! { }.into_any() } } }} } } #[derive(Clone, Debug, Error)] pub enum LoginError { #[error("Missing Password")] MissingPassword, #[error("Missing JID")] MissingJID, #[error("Invalid JID: {0}")] InvalidJID(#[from] jid::ParseError), #[error("Connection Error: {0}")] ConnectionError(#[from] CommandError), #[error("OPFS: {0}")] OPFS(#[from] OPFSError), } #[component] fn LoginPage( set_app: WriteSignal, set_client: WriteSignal)>>, ) -> impl IntoView { let jid = RwSignal::new("".to_string()); let password = RwSignal::new("".to_string()); let remember_me = RwSignal::new(false); let connect_on_login = RwSignal::new(true); let (error, set_error) = signal(None::); let error_message = move || { error.with(|error| { if let Some(error) = error { view! {
{error.to_string()}
}.into_any() } else { view! {}.into_any() } }) }; let (login_pending, set_login_pending) = signal(false); let login = Action::new_local(move |_| { async move { set_login_pending.set(true); if jid.read_untracked().is_empty() { set_error.set(Some(LoginError::MissingJID)); set_login_pending.set(false); return; } if password.read_untracked().is_empty() { set_error.set(Some(LoginError::MissingPassword)); set_login_pending.set(false); return; } let jid = match JID::from_str(&jid.read_untracked()) { Ok(j) => j, Err(e) => { set_error.set(Some(e.into())); set_login_pending.set(false); return; } }; let remember_me = remember_me.get_untracked(); // initialise the client let db = if remember_me { debug!("creating db in opfs"); Db::create_connect_and_migrate(jid.as_bare().to_string()) .await .unwrap() } else { debug!("creating db in memory"); Db::create_connect_and_migrate_memory().await.unwrap() }; let files = if remember_me { let opfs = FilesOPFS::new(jid.as_bare().to_string()).await; match opfs { Ok(f) => Files::Opfs(f), Err(e) => { set_error.set(Some(e.into())); set_login_pending.set(false); return; } } } else { Files::Mem(FilesMem::new()) }; let (client, updates) = filamento::Client::new( jid.clone(), password.read_untracked().clone(), db, files.clone(), ); // TODO: remember_me let resource = ArcRwSignal::new(None::); let client = Client { client, resource: resource.clone(), jid: Arc::new(jid.to_bare()), file_store: files, }; if *connect_on_login.read_untracked() { match client.connect().await { Ok(r) => { resource.set(Some(r)) } Err(e) => { set_error.set(Some(e.into())); set_login_pending.set(false); return; } } } // debug!("before setting app state"); set_client.set(Some((client, updates))); set_app.set(AppState::LoggedIn); } }); view! {

Macaw Instant Messenger

{error_message}
} } pub struct MessageSubscriptions { all: HashMap>, subset: HashMap>>, } impl MessageSubscriptions { pub fn new() -> Self { Self { all: HashMap::new(), subset: HashMap::new(), } } pub async fn broadcast(&mut self, to: BareJID, message: MacawMessage) { // subscriptions to all let mut removals = Vec::new(); for (id, sender) in &self.all { match sender.send((to.clone(), message.clone())).await { Ok(_) => {} Err(_) => { removals.push(*id); } } } for removal in removals { self.all.remove(&removal); } // subscriptions to specific chat if let Some(subscribers) = self.subset.get_mut(&to) { let mut removals = Vec::new(); for (id, sender) in &*subscribers { match sender.send(message.clone()).await { Ok(_) => {} Err(_) => { removals.push(*id); } } } for removal in removals { subscribers.remove(&removal); } if subscribers.is_empty() { self.subset.remove(&to); } } } pub fn subscribe_all(&mut self) -> (Uuid, Receiver<(BareJID, MacawMessage)>) { let (send, recv) = mpsc::channel(10); let id = Uuid::new_v4(); self.all.insert(id, send); (id, recv) } pub fn subscribe_chat(&mut self, chat: BareJID) -> (Uuid, Receiver) { let (send, recv) = mpsc::channel(10); let id = Uuid::new_v4(); if let Some(chat_subscribers) = self.subset.get_mut(&chat) { chat_subscribers.insert(id, send); } else { let hash_map = HashMap::from([(id, send)]); self.subset.insert(chat, hash_map); } (id, recv) } pub fn unsubscribe_all(&mut self, sub_id: Uuid) { self.all.remove(&sub_id); } pub fn unsubscribe_chat(&mut self, sub_id: Uuid, chat: BareJID) { if let Some(chat_subs) = self.subset.get_mut(&chat) { chat_subs.remove(&sub_id); } } } #[derive(Store, Clone)] pub struct Roster { #[store(key: BareJID = |(jid, _)| jid.clone())] contacts: HashMap, } impl Roster { pub fn new() -> Self { Self { contacts: HashMap::new(), } } } // TODO: multiple panels // pub struct OpenChats { // panels: // } #[derive(Store, Default)] pub struct OpenChatsPanel { // jid must be a chat in the chats map chat_view: Option, #[store(key: BareJID = |(jid, _)| jid.clone())] chats: IndexMap, } pub fn open_chat(open_chats: Store, chat: MacawChat) { if let Some(jid) = &*open_chats.chat_view().read() { if let Some((index, _jid, entry)) = open_chats.chats().write().shift_remove_full(jid) { let new_jid = as Clone>::clone(&chat.chat) .correspondent() .read() .clone(); open_chats .chats() .write() .insert_before(index, new_jid.clone(), chat); *open_chats.chat_view().write() = Some(new_jid); } else { let new_jid = as Clone>::clone(&chat.chat) .correspondent() .read() .clone(); open_chats.chats().write().insert(new_jid.clone(), chat); *open_chats.chat_view().write() = Some(new_jid); } } else { let new_jid = as Clone>::clone(&chat.chat) .correspondent() .read() .clone(); open_chats.chats().write().insert(new_jid.clone(), chat); *open_chats.chat_view().write() = Some(new_jid); } } impl OpenChatsPanel { pub fn open(&mut self, chat: MacawChat) { if let Some(jid) = &mut self.chat_view { debug!("a chat was already open"); if let Some((index, _jid, entry)) = self.chats.shift_remove_full(jid) { let new_jid = as Clone>::clone(&chat.chat) .correspondent() .read() .clone(); self.chats.insert_before(index, new_jid.clone(), chat); *&mut self.chat_view = Some(new_jid); } else { let new_jid = as Clone>::clone(&chat.chat) .correspondent() .read() .clone(); self.chats.insert(new_jid.clone(), chat); *&mut self.chat_view = Some(new_jid); } } else { let new_jid = as Clone>::clone(&chat.chat) .correspondent() .read() .clone(); self.chats.insert(new_jid.clone(), chat); *&mut self.chat_view = Some(new_jid); } debug!("opened chat"); } // TODO: // pub fn open_in_new_tab_unfocused(&mut self) { // } // pub fn open_in_new_tab_focus(&mut self) { // } } #[derive(Store)] pub struct UserPresences { #[store(key: BareJID = |(jid, _)| jid.clone())] user_presences: HashMap>, } 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: &BareJID) -> ArcRwSignal { if let Some(presences) = self.user_presences.get(user) { presences.clone() } else { let presences = Presences::new(); let signal = ArcRwSignal::new(presences); self.user_presences.insert(user.clone(), signal.clone()); 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 } 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, ); } 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] fn Macaw( // TODO: logout // app_state: WriteSignal)>, LocalStorage>, client: Client, mut updates: Receiver, set_app: WriteSignal, ) -> impl IntoView { provide_context(set_app); provide_context(client); let roster = Store::new(Roster::new()); provide_context(roster); let message_subscriptions = RwSignal::new(MessageSubscriptions::new()); provide_context(message_subscriptions); let messages_store: StateStore> = StateStore::new(); provide_context(messages_store); let chats_store: StateStore> = StateStore::new(); provide_context(chats_store); let users_store: StateStore> = StateStore::new(); provide_context(users_store); let open_chats = Store::new(OpenChatsPanel::default()); provide_context(open_chats); let show_settings = RwSignal::new(None::); provide_context(show_settings); let user_presences = Store::new(UserPresences::new()); provide_context(user_presences); let client_user = LocalResource::new(move || { async move { let client = use_context::().expect("client not in context"); let user = client.get_user((*client.jid).clone()).await.unwrap(); MacawUser::got_user(user) } }); provide_context(client_user); // TODO: timestamp incoming/outgoing subscription requests let (subscription_requests, set_subscription_requests)= signal(HashSet::::new()); provide_context(subscription_requests); provide_context(set_subscription_requests); // TODO: get cached contacts on login before getting the updated contacts OnceResource::new(async move { while let Some(update) = updates.recv().await { match update { UpdateMessage::Online(online, items) => { let contacts = items .into_iter() .map(|(contact, user)| { ( contact.user_jid.clone(), MacawContact::got_contact_and_user(contact, user), ) }) .collect(); roster.contacts().set(contacts); } 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) { macaw_contact.set(contact); } else { let jid = contact.user_jid.clone(); let contact = MacawContact::got_contact_and_user(contact, user); roster.insert(jid, contact); } }); } UpdateMessage::RosterDelete(jid) => { roster.contacts().update(|roster| { roster.remove(&jid); }); } UpdateMessage::Presence { from, presence } => { let bare_jid = from.to_bare(); if let Some(presences) = user_presences.read().user_presences.get(&bare_jid) { if let Some(resource) = from.resourcepart() { presences.write().update_presence(resource.clone(), presence); } } else { if let Some(resource) = from.resourcepart() { let mut presences = Presences::new(); presences.update_presence(resource.clone(), presence); user_presences.write().user_presences.insert(bare_jid, ArcRwSignal::new(presences)); } } } UpdateMessage::Message { to, from, message } => { debug!("before got message"); let new_message = MacawMessage::got_message_and_user(message, from); debug!("after got message"); spawn_local(async move { message_subscriptions .write() .broadcast(to, new_message) .await }); debug!("after set message"); } UpdateMessage::MessageDelivery { id, chat, delivery } => { messages_store.modify(&id, |message| { as Clone>::clone(&message) .delivery() .set(Some(delivery)) }); } UpdateMessage::SubscriptionRequest(jid) => { set_subscription_requests.update(|req| { req.insert(jid); }); } UpdateMessage::NickChanged { jid, nick } => { users_store.modify(&jid, |user| { user.update(|user| *&mut user.nick = nick.clone()) }); } UpdateMessage::AvatarChanged { jid, id } => { users_store.modify(&jid, |user| *&mut user.write().avatar = id.clone()); } } } }); view! { // {move || if let Some(_) = *show_settings.read() { view! { }.into_any() } else { view! {}.into_any() }} } } #[derive(PartialEq, Eq, Clone, Copy)] pub enum SidebarOpen { Roster, Chats, } /// returns whether the state was changed to open (true) or closed (false) pub fn toggle_open(state: &mut Option, 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> = use_context().expect("no pending subscriptions in context"); // for what has been clicked open (in the background) let (open, set_open) = signal(None::); // for what is just in the hovered state (not clicked to be pinned open yet necessarily) let (hovered, set_hovered) = signal(None::); let (just_closed, set_just_closed) = signal(false); view! { } } #[component] pub fn PersonalStatus() -> impl IntoView { let user: LocalResource = use_context().expect("no local user in context"); let (open, set_open) = signal(false); move || if let Some(user) = user.get() { let user: Store = as Clone>::clone(&(*user.user)).into(); view! {
{move || { let open = open.get(); debug!("open = {:?}", open); if open { view! { }.into_any() } else { view! {}.into_any() }}} }.into_any() } else { view! {}.into_any() } } #[component] pub fn PersonalStatusMenu(user: Store, set_open: WriteSignal) -> impl IntoView { let set_app: WriteSignal = use_context().unwrap(); let show_settings: RwSignal> = use_context().unwrap(); let user_presences: Store = use_context().expect("no user presence store"); let client = use_context::().expect("client not in context"); let client1 = client.clone(); let (show_value, set_show_value) = signal({ let show = match user_presences.write().get_user_presences(&user.jid().read()).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 = NodeRef::new(); let disconnect = Action::new_local(move |()| { let client = client.clone(); async move { client.disconnect(Offline::default()).await; } }); let set_status = Action::new_local(move |show_value: &i32| { let show_value = show_value.to_owned(); let client = client1.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! { } } #[component] pub fn Overlay(set_open: WriteSignal, children: Children) -> impl IntoView { view! {
{children()}
} } #[component] pub fn Modal(on_background_click: impl Fn(MouseEvent) + 'static, children: Children) -> impl IntoView { view! { } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SettingsPage { Account, Chat, Profile, Privacy, } #[component] pub fn Settings() -> impl IntoView { let show_settings: RwSignal> = use_context().unwrap(); view! {

Settings

Account
Chat
Privacy
Profile
{move || if let Some(page) = show_settings.get() { match page { SettingsPage::Account => view! {
"account"
}.into_any(), SettingsPage::Chat => view! {
"chat"
}.into_any(), SettingsPage::Profile => view! { }.into_any(), SettingsPage::Privacy => view! {
"privacy"
}.into_any(), } } else { view! {}.into_any() }}
} } #[component] pub fn ProfileSettings() -> impl IntoView { view! {
} } #[component] pub fn OpenChatsPanelView() -> impl IntoView { let open_chats: Store = use_context().expect("no open chats panel in context"); // TODO: tabs // view! { // {move || { // if open_chats.chats().read().len() > 1 { // Some( // view! { // // }, // ) // } else { // None // } // }} // } view! {
{move || { if let Some(open_chat) = open_chats.chat_view().get() { if let Some(open_chat) = open_chats.chats().read().get(&open_chat) { view! { }.into_any() } else { view! {}.into_any() } } else { view! {}.into_any() } }}
} } #[component] pub fn OpenChatView(chat: MacawChat) -> impl IntoView { let chat_chat: Store = as Clone>::clone(&chat.chat).into(); let chat_jid = move || chat_chat.correspondent().get(); view! {
} } 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) -> impl IntoView { let avatar = LocalResource::new(move || get_avatar(user)); let user_presences: Store = 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! {
{move || if let Some(icon) = show_icon() { view!{ }.into_any() } else { view! {}.into_any() }}
} } #[component] pub fn ChatViewHeader(chat: MacawChat) -> impl IntoView { let chat_user = as Clone>::clone(&chat.user).into(); let name = move || get_name(chat_user, true); let jid = move || chat_user.jid().read().to_string(); view! {
} } #[component] pub fn MessageHistoryBuffer(chat: MacawChat) -> impl IntoView { let (messages, set_messages) = arc_signal(IndexMap::new()); let chat_chat: Store = as Clone>::clone(&chat.chat).into(); let chat_user: Store = as Clone>::clone(&chat.user).into(); let load_set_messages = set_messages.clone(); let load_messages = LocalResource::new(move || { let load_set_messages = load_set_messages.clone(); async move { let client = use_context::().expect("client not in context"); let messages = client .get_messages_with_users(chat_chat.correspondent().get()) .await .map_err(|e| e.to_string()); match messages { Ok(m) => { let messages = m .into_iter() .map(|(message, message_user)| { ( message.id, MacawMessage::got_message_and_user(message, message_user), ) }) .collect::>(); load_set_messages.set(messages); } Err(err) => { error!("{err}") // TODO: show error message at top of chats list } } } }); // TODO: filter new messages signal let new_messages_signal: RwSignal = use_context().unwrap(); let (sub_id, set_sub_id) = signal(None); let load_new_messages_set = set_messages.clone(); let _load_new_messages = LocalResource::new(move || { let load_new_messages_set = load_new_messages_set.clone(); async move { load_messages.await; let (sub_id, mut new_messages) = new_messages_signal .write() .subscribe_chat(chat_chat.correspondent().get()); set_sub_id.set(Some(sub_id)); while let Some(new_message) = new_messages.recv().await { debug!("got new message in let message buffer"); let mut messages = load_new_messages_set.write(); if let Some((_, last)) = messages.last() { if * as Clone>::clone(&last.message) .timestamp() .read() < * as Clone>::clone(&new_message) .timestamp() .read() { messages.insert( as Clone>::clone( &new_message.message, ) .id() .get(), new_message, ); debug!("set the new message in message buffer"); } else { let index = match messages.binary_search_by(|_, value| { as Clone>::clone(&value.message) .timestamp() .read() .cmp( & as Clone>::clone( &new_message.message, ) .timestamp() .read(), ) }) { Ok(i) => i, Err(i) => i, }; messages.insert_before( // TODO: check if this logic is correct index, as Clone>::clone( &new_message.message, ) .id() .get(), new_message, ); debug!("set the new message in message buffer"); } } else { messages.insert( as Clone>::clone(&new_message.message) .id() .get(), new_message, ); debug!("set the new message in message buffer"); } } } }); on_cleanup(move || { if let Some(sub_id) = sub_id.get() { new_messages_signal .write() .unsubscribe_chat(sub_id, chat_chat.correspondent().get()); } }); let each = move || { let mut last_timestamp = NaiveDateTime::MIN; let mut last_user: Option = None; let mut messages = messages .get() .into_iter() .map(|(id, message)| { let message_timestamp = as Clone>::clone(&message.message) .timestamp() .read() .naive_local(); // TODO: mark new day // if message_timestamp.date() > last_timestamp.date() { // messages_view = messages_view.push(date(message_timestamp.date())); // } let major = if last_user.as_ref() != Some(&message.message.read().from) || message_timestamp - last_timestamp > TimeDelta::minutes(3) { true } else { false }; last_user = Some( as Clone>::clone(&message.message) .from() .get(), ); last_timestamp = message_timestamp; (id, (message, major, false)) }) .collect::>(); if let Some((_id, (_, _, last))) = messages.last_mut() { *last = true } messages.into_iter().rev() }; view! {
} } #[derive(Copy, Clone)] pub enum Icon { AddContact24, Attachment24, Away16, Away16Color, Bubble16, Bubble16Color, Bubble24, Close24, Contact24, Delivered16, Dnd16, Dnd16Color, Error16Color, Forward24, Heart24, NewBubble24, Reply24, Sending16, Sent16, Chat16Color, Xa16Color, Available16Color, } pub const ICONS_SRC: &str = "/assets/icons/"; impl Icon { pub fn src(&self) -> String { match self { Icon::AddContact24 => format!("{}addcontact24.svg", ICONS_SRC), Icon::Attachment24 => format!("{}attachment24.svg", ICONS_SRC), Icon::Away16 => format!("{}away16.svg", ICONS_SRC), Icon::Away16Color => format!("{}away16color.svg", ICONS_SRC), Icon::Bubble16 => format!("{}bubble16.svg", ICONS_SRC), Icon::Bubble16Color => format!("{}bubble16color.svg", ICONS_SRC), Icon::Bubble24 => format!("{}bubble24.svg", ICONS_SRC), Icon::Close24 => format!("{}close24.svg", ICONS_SRC), Icon::Contact24 => format!("{}contact24.svg", ICONS_SRC), Icon::Delivered16 => format!("{}delivered16.svg", ICONS_SRC), Icon::Dnd16 => format!("{}dnd16.svg", ICONS_SRC), Icon::Dnd16Color => format!("{}dnd16color.svg", ICONS_SRC), Icon::Error16Color => format!("{}error16color.svg", ICONS_SRC), Icon::Forward24 => format!("{}forward24.svg", ICONS_SRC), Icon::Heart24 => format!("{}heart24.svg", ICONS_SRC), Icon::NewBubble24 => format!("{}newbubble24.svg", ICONS_SRC), 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), } } pub fn size(&self) -> isize { match self { Icon::AddContact24 => 24, Icon::Attachment24 => 24, Icon::Away16 => 16, Icon::Away16Color => 16, Icon::Bubble16 => 16, Icon::Bubble16Color => 16, Icon::Bubble24 => 24, Icon::Close24 => 24, Icon::Contact24 => 24, Icon::Delivered16 => 16, Icon::Dnd16 => 16, Icon::Dnd16Color => 16, Icon::Error16Color => 16, Icon::Forward24 => 24, Icon::Heart24 => 24, Icon::NewBubble24 => 24, Icon::Reply24 => 24, Icon::Sending16 => 16, Icon::Sent16 => 16, Icon::Chat16Color => 16, Icon::Xa16Color => 16, Icon::Available16Color => 16, } } pub fn light(&self) -> bool { match self { Icon::AddContact24 => true, Icon::Attachment24 => true, Icon::Away16 => true, Icon::Away16Color => false, Icon::Bubble16 => true, Icon::Bubble16Color => false, Icon::Bubble24 => true, Icon::Close24 => true, Icon::Contact24 => true, Icon::Delivered16 => true, Icon::Dnd16 => true, Icon::Dnd16Color => false, Icon::Error16Color => false, Icon::Forward24 => true, Icon::Heart24 => true, Icon::NewBubble24 => true, Icon::Reply24 => true, Icon::Sending16 => true, Icon::Sent16 => true, Icon::Chat16Color => false, Icon::Xa16Color => false, Icon::Available16Color => false, } } } #[component] pub fn IconComponent(icon: Icon) -> impl IntoView { view! { } } #[component] pub fn Delivery(delivery: Delivery) -> impl IntoView { match delivery { // TODO: proper icon coloring/theming Delivery::Sending => { view! { } .into_any() } Delivery::Written => { view! { }.into_any() } // TODO: message receipts // Delivery::Written => view! {}.into_any(), Delivery::Sent => view! { }.into_any(), Delivery::Delivered => { view! { }.into_any() } // TODO: check if there is also the icon class Delivery::Read => { view! { } .into_any() } Delivery::Failed => { view! { } .into_any() } // TODO: queued icon Delivery::Queued => { view! { } .into_any() } } } #[component] pub fn Message(message: MacawMessage, major: bool, r#final: bool) -> impl IntoView { let message_message: Store = as Clone>::clone(&message.message).into(); let message_user = as Clone>::clone(&message.user).into(); let avatar = LocalResource::new(move || get_avatar(message_user)); let name = move || get_name(message_user, false); // TODO: chrono-humanize? // TODO: if final, show delivery not only on hover. // {move || message_message.delivery().read().map(|delivery| delivery.to_string()).unwrap_or_default()} if major { view! {
} >
{name}
{move || message_message.timestamp().read().format("%H:%M").to_string()}
{move || message_message.body().read().body.clone()}
{move || message_message.delivery().get().map(|delivery| view! { } ) }
}.into_any() } else { view! {
{move || message_message.timestamp().read().format("%H:%M").to_string()}
{move || message_message.body().read().body.clone()}
{move || message_message.delivery().get().map(|delivery| view! { } ) }
}.into_any() } } #[component] pub fn ChatViewMessageComposer(chat: BareJID) -> impl IntoView { let message_input: NodeRef
= NodeRef::new(); // TODO: load last message draft let new_message = RwSignal::new("".to_string()); let client: Client = use_context().expect("no client in context"); let client = RwSignal::new(client); let (shift_pressed, set_shift_pressed) = signal(false); let send_message = move || { let value = chat.clone(); spawn_local(async move { match client .read() .send_message( value, Body { body: new_message.get(), }, ) .await { Ok(_) => { new_message.set("".to_string()); message_input .write() .as_ref() .expect("message input div not mounted") .set_text_content(Some("")); } Err(e) => tracing::error!("message send error: {}", e), } }) }; let _focus = Effect::new(move |_| { if let Some(input) = message_input.get() { let _ = input.focus(); // TODO: set the last draft input.set_text_content(Some("")); // input.style("height: 0"); // let height = input.scroll_height(); // input.style(format!("height: {}px", height)); } }); // let on_input = move |ev: Event| { // // let keyboard_event: KeyboardEvent = ev.try_into().unwrap(); // debug!("got input event"); // let key= event_target_value(&ev); // new_message.set(key); // debug!("set new message"); // }; // // TODO: placeholder view! {
set_shift_pressed.set(true), 13 => if !shift_pressed.get() { ev.prevent_default(); send_message(); } _ => {} // debug!("shift pressed down"); } } on:keyup=move |ev| { match ev.key_code() { 16 => set_shift_pressed.set(false), _ => {} // debug!("shift released"); } } >
//
} } // V has to be an arc signal #[derive(Debug)] struct ArcStateStore { store: Arc>>, } impl PartialEq for ArcStateStore { fn eq(&self, other: &Self) -> bool { Arc::ptr_eq(&self.store, &other.store) } } impl Clone for ArcStateStore { fn clone(&self) -> Self { Self { store: Arc::clone(&self.store), } } } impl Eq for ArcStateStore {} impl ArcStateStore { pub fn new() -> Self { Self { store: Arc::new(RwLock::new(HashMap::new())), } } } #[derive(Debug)] struct StateStore { inner: ArenaItem, S>, } impl Dispose for StateStore { fn dispose(self) { self.inner.dispose() } } impl StateStore where K: Send + Sync + 'static, V: Send + Sync + 'static, { pub fn new() -> Self { Self::new_with_storage() } } impl StateStore where K: 'static, V: 'static, S: Storage>, { pub fn new_with_storage() -> Self { Self { inner: ArenaItem::new_with_storage(ArcStateStore::new()), } } } impl StateStore where K: 'static, V: 'static, { pub fn new_local() -> Self { Self::new_with_storage() } } impl< K: std::marker::Send + std::marker::Sync + 'static, V: std::marker::Send + std::marker::Sync + 'static, > From> for StateStore { fn from(value: ArcStateStore) -> Self { Self { inner: ArenaItem::new_with_storage(value), } } } impl FromLocal> for StateStore { fn from_local(value: ArcStateStore) -> Self { Self { inner: ArenaItem::new_with_storage(value), } } } impl Copy for StateStore {} impl Clone for StateStore { fn clone(&self) -> Self { *self } } impl StateStore where K: Send + Sync + 'static, V: Send + Sync + 'static, { pub fn store(&self, key: K, value: V) -> StateListener { { let store = self.inner.try_get_value().unwrap(); let mut store = store.store.write().unwrap(); if let Some((v, count)) = store.get_mut(&key) { *v = value.clone(); *count += 1; } else { store.insert(key.clone(), (value.clone(), 1)); } }; StateListener { value, cleaner: StateCleaner { key, state_store: self.clone(), }, } } } impl StateStore where K: Eq + std::hash::Hash + Send + Sync + 'static, V: Send + Sync + 'static, { pub fn update(&self, key: &K, value: V) { let store = self.inner.try_get_value().unwrap(); let mut store = store.store.write().unwrap(); if let Some((v, _)) = store.get_mut(key) { *v = value; } } pub fn modify(&self, key: &K, modify: impl Fn(&mut V)) { let store = self.inner.try_get_value().unwrap(); let mut store = store.store.write().unwrap(); if let Some((v, _)) = store.get_mut(key) { modify(v); } } fn remove(&self, key: &K) { // let store = self.inner.try_get_value().unwrap(); // let mut store = store.store.write().unwrap(); // if let Some((_v, count)) = store.get_mut(key) { // *count -= 1; // if *count == 0 { // store.remove(key); // debug!("dropped item from store"); // } // } } } #[derive(Clone)] struct StateListener where K: Eq + std::hash::Hash + 'static + std::marker::Send + std::marker::Sync, V: 'static + std::marker::Send + std::marker::Sync, { value: V, cleaner: StateCleaner, } impl< K: std::cmp::Eq + std::hash::Hash + std::marker::Send + std::marker::Sync, V: std::marker::Send + std::marker::Sync, > Deref for StateListener { type Target = V; fn deref(&self) -> &Self::Target { &self.value } } impl DerefMut for StateListener { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.value } } struct ArcStateCleaner { key: K, state_store: ArcStateStore, } struct StateCleaner where K: Eq + std::hash::Hash + Send + Sync + 'static, V: Send + Sync + 'static, { key: K, state_store: StateStore, } impl Clone for StateCleaner where K: Eq + std::hash::Hash + Clone + Send + Sync, V: Send + Sync, { fn clone(&self) -> Self { { let store = self.state_store.inner.try_get_value().unwrap(); let mut store = store.store.write().unwrap(); if let Some((_v, count)) = store.get_mut(&self.key) { *count += 1; } } Self { key: self.key.clone(), state_store: self.state_store.clone(), } } } impl Drop for StateCleaner { fn drop(&mut self) { self.state_store.remove(&self.key); } } #[derive(Clone)] struct MacawChat { chat: StateListener>, user: StateListener>, } impl MacawChat { fn got_chat_and_user(chat: Chat, user: User) -> Self { let chat_state_store: StateStore> = use_context().expect("no chat state store"); let user_state_store: StateStore> = use_context().expect("no user state store"); let user = user_state_store.store(user.jid.clone(), ArcStore::new(user)); let chat = chat_state_store.store(chat.correspondent.clone(), ArcStore::new(chat)); Self { chat, user } } } impl Deref for MacawChat { type Target = StateListener>; fn deref(&self) -> &Self::Target { &self.chat } } impl DerefMut for MacawChat { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.chat } } #[derive(Clone)] struct MacawMessage { message: StateListener>, user: StateListener>, } impl MacawMessage { fn got_message_and_user(message: Message, user: User) -> Self { let message_state_store: StateStore> = use_context().expect("no message state store"); let user_state_store: StateStore> = use_context().expect("no user state store"); let message = message_state_store.store(message.id, ArcStore::new(message)); let user = user_state_store.store(user.jid.clone(), ArcStore::new(user)); Self { message, user } } } impl Deref for MacawMessage { type Target = StateListener>; fn deref(&self) -> &Self::Target { &self.message } } impl DerefMut for MacawMessage { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.message } } #[derive(Clone)] struct MacawUser { user: StateListener>, } impl MacawUser { fn got_user(user: User) -> Self { let user_state_store: StateStore> = 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>; fn deref(&self) -> &Self::Target { &self.user } } impl DerefMut for MacawUser { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.user } } #[derive(Clone)] struct MacawContact { contact: Store, user: StateListener>, } impl MacawContact { fn got_contact_and_user(contact: Contact, user: User) -> Self { let contact = Store::new(contact); let user_state_store: StateStore> = use_context().expect("no user state store"); let user = user_state_store.store(user.jid.clone(), ArcStore::new(user)); Self { contact, user } } } impl Deref for MacawContact { type Target = Store; fn deref(&self) -> &Self::Target { &self.contact } } impl DerefMut for MacawContact { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.contact } } #[component] fn ChatsList() -> impl IntoView { let (chats, set_chats) = signal(IndexMap::new()); let load_chats = LocalResource::new(move || async move { let client = use_context::().expect("client not in context"); let chats = client .get_chats_ordered_with_latest_messages_and_users() .await .map_err(|e| e.to_string()); match chats { Ok(c) => { let chats = c .into_iter() .map(|((chat, chat_user), (message, message_user))| { ( chat.correspondent.clone(), ( MacawChat::got_chat_and_user(chat, chat_user), MacawMessage::got_message_and_user(message, message_user), ), ) }) .collect::>(); set_chats.set(chats); } Err(_) => { // TODO: show error message at top of chats list } } }); let (open_new_chat, set_open_new_chat) = signal(false); // TODO: filter new messages signal let new_messages_signal: RwSignal = use_context().unwrap(); let (sub_id, set_sub_id) = signal(None); let _load_new_messages = LocalResource::new(move || async move { load_chats.await; let (sub_id, mut new_messages) = new_messages_signal.write().subscribe_all(); set_sub_id.set(Some(sub_id)); while let Some((to, new_message)) = new_messages.recv().await { debug!("got new message in let"); let mut chats = set_chats.write(); if let Some((chat, _latest_message)) = chats.shift_remove(&to) { // TODO: check if new message is actually latest message debug!("chat existed"); debug!("new message: {}", new_message.read().body.body); chats.insert_before(0, to, (chat.clone(), new_message)); debug!("done setting"); } else { debug!("the chat didn't exist"); let client = use_context::().expect("client not in context"); let chat = client.get_chat(to.clone()).await.unwrap(); let user = client.get_user(to.clone()).await.unwrap(); debug!("before got chat"); let chat = MacawChat::got_chat_and_user(chat, user); debug!("after got chat"); chats.insert_before(0, to, (chat, new_message)); debug!("done setting"); } } debug!("set the new message"); }); on_cleanup(move || { if let Some(sub_id) = sub_id.get() { new_messages_signal.write().unsubscribe_all(sub_id); } }); view! {
// TODO: update icon, tooltip on hover.

Chats

{move || { if *open_new_chat.read() { view! { }.into_any() } else { view! {}.into_any() } }}
} } #[derive(Clone, Debug, Error)] pub enum NewChatError { #[error("Missing JID")] MissingJID, #[error("Invalid JID: {0}")] InvalidJID(#[from] jid::ParseError), #[error("Database: {0}")] Db(#[from] CommandError), } #[component] fn NewChatWidget(set_open_new_chat: WriteSignal) -> impl IntoView { let jid = RwSignal::new("".to_string()); // TODO: compartmentalise into error component, form component... let (error, set_error) = signal(None::); let error_message = move || { error.with(|error| { if let Some(error) = error { view! {
{error.to_string()}
}.into_any() } else { view! {}.into_any() } }) }; let (new_chat_pending, set_new_chat_pending) = signal(false); let open_chats: Store = use_context().expect("no open chats panel store in context"); let client = use_context::().expect("client not in context"); let chat_state_store: StateStore> = use_context().expect("no chat state store"); let user_state_store: StateStore> = use_context().expect("no user state store"); let open_chat = Action::new_local(move |_| { let client = client.clone(); async move { set_new_chat_pending.set(true); if jid.read_untracked().is_empty() { set_error.set(Some(NewChatError::MissingJID)); set_new_chat_pending.set(false); return; } let jid = match JID::from_str(&jid.read_untracked()) { // TODO: ability to direct address a resource? Ok(j) => j.to_bare(), Err(e) => { set_error.set(Some(e.into())); set_new_chat_pending.set(false); return; } }; let chat_jid = jid; let (chat, user) = match client.get_chat_and_user(chat_jid).await { Ok(c) => c, Err(e) => { set_error.set(Some(e.into())); set_new_chat_pending.set(false); return; }, }; let chat = { let user = user_state_store.store(user.jid.clone(), ArcStore::new(user)); let chat = chat_state_store.store(chat.correspondent.clone(), ArcStore::new(chat)); MacawChat { chat, user } }; open_chats.update(|open_chats| open_chats.open(chat.clone())); set_open_new_chat.set(false); } }); let jid_input = NodeRef::::new(); let _focus = Effect::new(move |_| { if let Some(input) = jid_input.get() { let _ = input.focus(); input.set_text_content(Some("")); // input.style("height: 0"); // let height = input.scroll_height(); // input.style(format!("height: {}px", height)); } }); view! {
{error_message}
} } #[component] fn RosterList() -> impl IntoView { let requests: ReadSignal> = use_context().expect("no pending subscriptions in context"); let roster: Store = use_context().expect("no roster in context"); let (open_add_contact, set_open_add_contact) = signal(false); // TODO: filter new messages signal view! {

Roster

{move || { if !requests.read().is_empty() { view! {
}.into_any() } else { view! {}.into_any() } }}
{move || { if *open_add_contact.read() { view! {
}.into_any() } else { view! {}.into_any() } }}
} } #[derive(Clone, Debug, Error)] pub enum AddContactError { #[error("Missing JID")] MissingJID, #[error("Invalid JID: {0}")] InvalidJID(#[from] jid::ParseError), #[error("Subscription: {0}")] Db(#[from] CommandError), } #[component] fn AddContact() -> impl IntoView { let requests: ReadSignal> = use_context().expect("no pending subscriptions in context"); let set_requests: WriteSignal> = use_context().expect("no pending subscriptions write signal in context"); let roster: Store = use_context().expect("no roster in context"); let jid = RwSignal::new("".to_string()); // TODO: compartmentalise into error component, form component... let (error, set_error) = signal(None::); let error_message = move || { error.with(|error| { if let Some(error) = error { view! {
{error.to_string()}
}.into_any() } else { view! {}.into_any() } }) }; let (add_contact_pending, set_add_contact_pending) = signal(false); let client = use_context::().expect("client not in context"); let client2 = client.clone(); let client3 = client.clone(); let client4 = client.clone(); let add_contact= Action::new_local(move |_| { let client = client.clone(); async move { set_add_contact_pending.set(true); if jid.read_untracked().is_empty() { set_error.set(Some(AddContactError::MissingJID)); set_add_contact_pending.set(false); return; } let jid = match JID::from_str(&jid.read_untracked()) { Ok(j) => j.to_bare(), Err(e) => { set_error.set(Some(e.into())); set_add_contact_pending.set(false); return; } }; let chat_jid = jid; // TODO: more options? match client.buddy_request(chat_jid).await { Ok(c) => c, Err(e) => { set_error.set(Some(e.into())); set_add_contact_pending.set(false); return; }, }; set_add_contact_pending.set(false); } }); let jid_input = NodeRef::::new(); let _focus = Effect::new(move |_| { if let Some(input) = jid_input.get() { let _ = input.focus(); input.set_text_content(Some("")); // input.style("height: 0"); // let height = input.scroll_height(); // input.style(format!("height: {}px", height)); } }); let outgoing = move || roster.contacts().get().into_iter().filter(|(jid, contact)| { match *contact.contact.subscription().read() { filamento::roster::Subscription::None => false, filamento::roster::Subscription::PendingOut => true, filamento::roster::Subscription::PendingIn => false, filamento::roster::Subscription::PendingInPendingOut => true, filamento::roster::Subscription::OnlyOut => false, filamento::roster::Subscription::OnlyIn => false, filamento::roster::Subscription::OutPendingIn => false, filamento::roster::Subscription::InPendingOut => true, filamento::roster::Subscription::Buddy => false, } }).collect::>(); let accept_friend_request = Action::new_local(move |jid: &BareJID| { let client = client2.clone(); let jid = jid.clone(); async move { // TODO: error client.accept_buddy_request(jid).await; } }); let reject_friend_request = Action::new_local(move |jid: &BareJID| { let client = client3.clone(); let jid = jid.clone(); async move { // TODO: error client.unsubscribe_contact(jid.clone()).await; set_requests.write().remove(&jid); } }); let cancel_subscription_request = Action::new_local(move |jid: &BareJID| { let client = client4.clone(); let jid = jid.clone(); async move { // TODO: error client.unsubscribe_from_contact(jid).await; } }); view! {
{error_message}
{move || if !requests.read().is_empty() { view! {

Incoming Subscription Requests

{ let request2 = request.clone(); let request3 = request.clone(); let jid_string = move || request.to_string(); view! {
{jid_string}
Accept
Reject
} }
}.into_any() } else { view! {}.into_any() }} {move || if !outgoing().is_empty() { view! {

Pending Outgoing Subscription Requests

{ let jid2 = jid.clone(); let jid_string = move || jid.to_string(); view! {
{jid_string}
Cancel
} }
}.into_any() } else { view! {}.into_any() }}
} } #[component] fn RosterListItem(contact: MacawContact) -> impl IntoView { let contact_contact: Store = contact.contact; let contact_user: Store = as Clone>::clone(&contact.user).into(); let name = move || get_name(contact_user, false); let open_chats: Store = use_context().expect("no open chats panel store in context"); // TODO: why can this not be in the closure????? // TODO: not good, as overwrites preexisting chat state with possibly incorrect one... let chat = Chat { correspondent: contact_user.jid().get(), have_chatted: false, }; let chat = MacawChat::got_chat_and_user(chat, contact_user.get()); let open_chat = move |_| { debug!("opening chat"); open_chats.update(|open_chats| open_chats.open(chat.clone())); }; let open = move || { if let Some(open_chat) = &*open_chats.chat_view().read() { debug!("got open chat: {:?}", open_chat); if *open_chat == *contact_user.jid().read() { return Open::Focused; } } if let Some(_backgrounded_chat) = open_chats .chats() .read() .get(contact_user.jid().read().deref()) { return Open::Open; } Open::Closed }; let focused = move || open().is_focused(); let open = move || open().is_open(); view! {

{name} - {move || contact_contact.user_jid().read().to_string()}

{move || contact_contact.subscription().read().to_string()}
} } pub async fn get_avatar(user: Store) -> String { if let Some(avatar) = &user.read().avatar { let client = use_context::().expect("client not in context"); if let Some(data) = client.file_store.get_src(avatar).await { data } else { NO_AVATAR.to_string() } // TODO: enable avatar fetching // format!("/files/{}", avatar) } else { NO_AVATAR.to_string() } } pub fn get_name(user: Store, note_to_self: bool) -> String { let roster: Store = use_context().expect("no roster in context"); if note_to_self { let client: Client = use_context().expect("no client in context"); if *client.jid == *user.jid().read() { return "Note to self".to_string() } } if let Some(name) = roster .contacts() .read() .get(&user.read().jid) .map(|contact| contact.read().name.clone()) .unwrap_or_default() { name.to_string() } else if let Some(nick) = &user.read().nick { nick.to_string() } else { user.read().jid.to_string() } } 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, } } } #[component] fn ChatsListItem(chat: MacawChat, message: MacawMessage) -> impl IntoView { let chat_chat: Store = as Clone>::clone(&chat.chat).into(); let chat_user: Store = as Clone>::clone(&chat.user).into(); let message_message: Store = as Clone>::clone(&message.message).into(); let name = move || get_name(chat_user, true); // TODO: store fine-grained reactivity let latest_message_body = move || message_message.body().get().body; let open_chats: Store = use_context().expect("no open chats panel store in context"); let open_chat = move |_| { debug!("opening chat"); open_chats.update(|open_chats| open_chats.open(chat.clone())); }; let open = move || { if let Some(open_chat) = &*open_chats.chat_view().read() { debug!("got open chat: {:?}", open_chat); if *open_chat == *chat_chat.correspondent().read() { return Open::Focused; } } if let Some(_backgrounded_chat) = open_chats .chats() .read() .get(chat_chat.correspondent().read().deref()) { return Open::Open; } Open::Closed }; let focused = move || open().is_focused(); let open = move || open().is_open(); let date = move || message_message.timestamp().read().naive_local(); let now = move || Local::now().naive_local(); let timeinfo = move || if date().date() == now().date() { // TODO: localisation/config date().time().format("%H:%M").to_string() } else { date().date().format("%d/%m").to_string() }; view! {

{name}

{timeinfo}

{latest_message_body}

} }