diff options
author | 2025-05-08 23:31:10 +0100 | |
---|---|---|
committer | 2025-05-08 23:31:10 +0100 | |
commit | 19ba234341821bc68f3d06787ee15e5342fcaea8 (patch) | |
tree | 3bc4a4d6da92e55d6b72ad72de394b97e195fdeb /src | |
parent | 39a35689d385c89f79f97ce69bf214308be5e2fd (diff) | |
download | macaw-web-19ba234341821bc68f3d06787ee15e5342fcaea8.tar.gz macaw-web-19ba234341821bc68f3d06787ee15e5342fcaea8.tar.bz2 macaw-web-19ba234341821bc68f3d06787ee15e5342fcaea8.zip |
add rust-toolchain.toml + error log + rustfmt
Diffstat (limited to 'src')
-rw-r--r-- | src/lib.rs | 633 |
1 files changed, 426 insertions, 207 deletions
@@ -6,28 +6,44 @@ use std::{ ops::{Deref, DerefMut}, rc::Rc, str::FromStr, - sync::{atomic::AtomicUsize, Arc, RwLock}, + sync::{Arc, RwLock, atomic::AtomicUsize}, thread::sleep, time::{self, Duration}, }; -use base64::{prelude::BASE64_STANDARD, Engine}; +use base64::{Engine, prelude::BASE64_STANDARD}; use chrono::{NaiveDateTime, TimeDelta}; use filamento::{ - chat::{Body, Chat, ChatStoreFields, Delivery, Message, MessageStoreFields}, db::Db, error::{CommandError, ConnectionError, DatabaseError}, files::FileStore, files::opfs::OPFSError, files::FilesMem, files::FilesOPFS, roster::{Contact, ContactStoreFields}, user::{User, UserStoreFields}, UpdateMessage + 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}, }; use futures::stream::StreamExt; use indexmap::IndexMap; use jid::JID; use leptos::{ - ev::{Event, KeyboardEvent, SubmitEvent}, html::{self, Div, Input, Pre, Textarea}, prelude::*, tachys::{dom::document, reactive_graph::bind::GetValue}, task::{spawn, spawn_local} + ev::{Event, KeyboardEvent, SubmitEvent}, + html::{self, Div, Input, Pre, Textarea}, + prelude::*, + tachys::{dom::document, reactive_graph::bind::GetValue}, + task::{spawn, spawn_local}, }; use leptos_meta::Stylesheet; -use leptos_use::{use_textarea_autosize, UseTextareaAutosizeReturn}; +use leptos_use::{UseTextareaAutosizeReturn, use_textarea_autosize}; use reactive_stores::{ArcStore, Store, StoreField}; use stylance::import_style; use thiserror::Error; -use tokio::sync::{mpsc::{self, Receiver}, Mutex}; +use tokio::sync::{ + Mutex, + mpsc::{self, Receiver}, +}; use tracing::{debug, error}; use uuid::Uuid; @@ -61,11 +77,7 @@ impl FileStore for Files { } } - async fn store( - &self, - name: &str, - data: &[u8], - ) -> Result<(), Self::Err> { + 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?), @@ -90,7 +102,7 @@ impl Files { } else { None } - }, + } Files::Opfs(files_opfs) => files_opfs.get_src(file_name).await.ok(), } } @@ -145,7 +157,10 @@ pub enum LoginError { } #[component] -fn LoginPage(set_app: WriteSignal<AppState>, set_client: RwSignal<Option<(Client, Receiver<UpdateMessage>)>>) -> impl IntoView { +fn LoginPage( + set_app: WriteSignal<AppState>, + set_client: RwSignal<Option<(Client, Receiver<UpdateMessage>)>>, +) -> impl IntoView { let jid = RwSignal::new("".to_string()); let password = RwSignal::new("".to_string()); let remember_me = RwSignal::new(false); @@ -171,13 +186,13 @@ fn LoginPage(set_app: WriteSignal<AppState>, set_client: RwSignal<Option<(Client if jid.read_untracked().is_empty() { set_error.set(Some(LoginError::MissingJID)); set_login_pending.set(false); - return + return; } if password.read_untracked().is_empty() { set_error.set(Some(LoginError::MissingPassword)); set_login_pending.set(false); - return + return; } let jid = match JID::from_str(&jid.read_untracked()) { @@ -185,14 +200,16 @@ fn LoginPage(set_app: WriteSignal<AppState>, set_client: RwSignal<Option<(Client Err(e) => { set_error.set(Some(e.into())); set_login_pending.set(false); - return - }, + return; + } }; // initialise the client let db = if remember_me.get_untracked() { debug!("creating db in opfs"); - Db::create_connect_and_migrate(jid.as_bare().to_string()).await.unwrap() + 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() @@ -204,14 +221,18 @@ fn LoginPage(set_app: WriteSignal<AppState>, set_client: RwSignal<Option<(Client Err(e) => { set_error.set(Some(e.into())); set_login_pending.set(false); - return - }, + return; + } } } else { Files::Mem(FilesMem::new()) }; - let (client, updates) = - filamento::Client::new(jid.clone(), password.read_untracked().clone(), db, files.clone()); + let (client, updates) = filamento::Client::new( + jid.clone(), + password.read_untracked().clone(), + db, + files.clone(), + ); // TODO: remember_me let client = Client { client, @@ -221,12 +242,12 @@ fn LoginPage(set_app: WriteSignal<AppState>, set_client: RwSignal<Option<(Client if *connect_on_login.read_untracked() { match client.connect().await { - Ok(_) => {}, + Ok(_) => {} Err(e) => { set_error.set(Some(e.into())); set_login_pending.set(false); - return - }, + return; + } } } @@ -307,17 +328,12 @@ impl MessageSubscriptions { } } - pub async fn broadcast( - &mut self, - to: JID, - message: MacawMessage, - ) { + pub async fn broadcast(&mut self, to: JID, 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(_) => { - } + Ok(_) => {} Err(_) => { removals.push(*id); } @@ -332,8 +348,7 @@ impl MessageSubscriptions { let mut removals = Vec::new(); for (id, sender) in &*subscribers { match sender.send(message.clone()).await { - Ok(_) => { - } + Ok(_) => {} Err(_) => { removals.push(*id); } @@ -348,19 +363,14 @@ impl MessageSubscriptions { } } - pub fn subscribe_all( - &mut self, - ) -> (Uuid, Receiver<(JID, MacawMessage)>) { + pub fn subscribe_all(&mut self) -> (Uuid, Receiver<(JID, 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: JID, - ) -> (Uuid, Receiver<MacawMessage>) { + pub fn subscribe_chat(&mut self, chat: JID) -> (Uuid, Receiver<MacawMessage>) { let (send, recv) = mpsc::channel(10); let id = Uuid::new_v4(); if let Some(chat_subscribers) = self.subset.get_mut(&chat) { @@ -399,7 +409,7 @@ impl Roster { // TODO: multiple panels // pub struct OpenChats { -// panels: +// panels: // } #[derive(Store, Default)] @@ -413,16 +423,28 @@ pub struct OpenChatsPanel { pub fn open_chat(open_chats: Store<OpenChatsPanel>, 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 = <ArcStore<filamento::chat::Chat> as Clone>::clone(&chat.chat).correspondent().read().clone(); - open_chats.chats().write().insert_before(index, new_jid.clone(), chat); + let new_jid = <ArcStore<filamento::chat::Chat> 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 = <ArcStore<filamento::chat::Chat> as Clone>::clone(&chat.chat).correspondent().read().clone(); + let new_jid = <ArcStore<filamento::chat::Chat> 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 = <ArcStore<filamento::chat::Chat> as Clone>::clone(&chat.chat).correspondent().read().clone(); + let new_jid = <ArcStore<filamento::chat::Chat> 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); } @@ -433,16 +455,25 @@ impl OpenChatsPanel { 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 = <ArcStore<filamento::chat::Chat> as Clone>::clone(&chat.chat).correspondent().read().clone(); + let new_jid = <ArcStore<filamento::chat::Chat> 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 = <ArcStore<filamento::chat::Chat> as Clone>::clone(&chat.chat).correspondent().read().clone(); + let new_jid = <ArcStore<filamento::chat::Chat> 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 = <ArcStore<filamento::chat::Chat> as Clone>::clone(&chat.chat).correspondent().read().clone(); + let new_jid = <ArcStore<filamento::chat::Chat> as Clone>::clone(&chat.chat) + .correspondent() + .read() + .clone(); self.chats.insert(new_jid.clone(), chat); *&mut self.chat_view = Some(new_jid); } @@ -451,11 +482,11 @@ impl OpenChatsPanel { // TODO: // pub fn open_in_new_tab_unfocused(&mut self) { - + // } // pub fn open_in_new_tab_focus(&mut self) { - + // } } @@ -490,12 +521,18 @@ fn Macaw( 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(); + 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) => {}, + } + UpdateMessage::Offline(offline) => {} UpdateMessage::RosterUpdate(contact, user) => { roster.contacts().update(|roster| { if let Some(macaw_contact) = roster.get_mut(&contact.user_jid) { @@ -506,30 +543,41 @@ fn Macaw( roster.insert(jid, contact); } }); - }, + } UpdateMessage::RosterDelete(jid) => { roster.contacts().update(|roster| { roster.remove(&jid); }); - }, - UpdateMessage::Presence { from, presence } => {}, + } + UpdateMessage::Presence { from, presence } => {} 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_untracked().broadcast(to, new_message).await }); + spawn_local(async move { + message_subscriptions + .write_untracked() + .broadcast(to, new_message) + .await + }); debug!("after set message"); - }, + } UpdateMessage::MessageDelivery { id, chat, delivery } => { - messages_store.modify(&id, |message| <ArcStore<filamento::chat::Message> as Clone>::clone(&message).delivery().set(Some(delivery))); - }, - UpdateMessage::SubscriptionRequest(jid) => {}, + messages_store.modify(&id, |message| { + <ArcStore<filamento::chat::Message> as Clone>::clone(&message) + .delivery() + .set(Some(delivery)) + }); + } + UpdateMessage::SubscriptionRequest(jid) => {} UpdateMessage::NickChanged { jid, nick } => { - users_store.modify(&jid, |user| user.update(|user| *&mut user.nick = nick.clone())); - }, + 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()); - }, + } } } }); @@ -549,11 +597,13 @@ pub enum SidebarOpen { pub fn toggle_open(state: &mut Option<SidebarOpen>, open: SidebarOpen) { match state { - Some(opened) => if *opened == open { - *state = None - } else { - *state = Some(open) - }, + Some(opened) => { + if *opened == open { + *state = None + } else { + *state = Some(open) + } + } None => *state = Some(open), } } @@ -595,7 +645,7 @@ pub fn Sidebar() -> impl IntoView { SidebarOpen::Chats => view! { <ChatsList /> }.into_any(), - } + } } else { view! {}.into_any() }} @@ -644,7 +694,8 @@ pub fn OpenChatsPanelView() -> impl IntoView { #[component] pub fn OpenChatView(chat: MacawChat) -> impl IntoView { - let chat_chat: Store<Chat> = <ArcStore<filamento::chat::Chat> as Clone>::clone(&chat.chat).into(); + let chat_chat: Store<Chat> = + <ArcStore<filamento::chat::Chat> as Clone>::clone(&chat.chat).into(); let chat_jid = move || chat_chat.correspondent().get(); view! { @@ -678,88 +729,153 @@ pub fn ChatViewHeader(chat: MacawChat) -> impl IntoView { #[component] pub fn MessageHistoryBuffer(chat: MacawChat) -> impl IntoView { - let (messages, set_messages) = signal(IndexMap::new()); - let chat_chat: Store<Chat> = <ArcStore<filamento::chat::Chat> as Clone>::clone(&chat.chat).into(); - let chat_user: Store<User> = <ArcStore<filamento::user::User> as Clone>::clone(&chat.user).into(); - - let load_messages = LocalResource::new(move || async move { - let client = use_context::<Client>().expect("client not in context"); - let messages = client.get_messages_with_users(chat_chat.correspondent().get_untracked()).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::<IndexMap<Uuid, _>>(); - set_messages.set(messages); - }, - Err(_) => { - // TODO: show error message at top of chats list - }, + let (messages, set_messages) = arc_signal(IndexMap::new()); + let chat_chat: Store<Chat> = + <ArcStore<filamento::chat::Chat> as Clone>::clone(&chat.chat).into(); + let chat_user: Store<User> = + <ArcStore<filamento::user::User> 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::<Client>().expect("client not in context"); + let messages = client + .get_messages_with_users(chat_chat.correspondent().get_untracked()) + .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::<IndexMap<Uuid, _>>(); + 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<MessageSubscriptions> = use_context().unwrap(); let (sub_id, set_sub_id) = signal(None); - let _load_new_messages = LocalResource::new(move || async move { - load_messages.await; - let (sub_id, mut new_messages) = new_messages_signal.write().subscribe_chat(chat_chat.correspondent().get_untracked()); - 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= set_messages.write(); - if let Some((_, last)) = messages.last() { - if *<ArcStore<filamento::chat::Message> as Clone>::clone(&last.message).timestamp().read_untracked() < *<ArcStore<filamento::chat::Message> as Clone>::clone(&new_message).timestamp().read_untracked() { - messages - .insert(<ArcStore<filamento::chat::Message> as Clone>::clone(&new_message.message).id().get_untracked(), new_message); - debug!("set the new message in message buffer"); + 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_untracked()); + 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 *<ArcStore<filamento::chat::Message> as Clone>::clone(&last.message) + .timestamp() + .read_untracked() + < *<ArcStore<filamento::chat::Message> as Clone>::clone(&new_message) + .timestamp() + .read_untracked() + { + messages.insert( + <ArcStore<filamento::chat::Message> as Clone>::clone( + &new_message.message, + ) + .id() + .get_untracked(), + new_message, + ); + debug!("set the new message in message buffer"); + } else { + let index = match messages.binary_search_by(|_, value| { + <ArcStore<filamento::chat::Message> as Clone>::clone(&value.message) + .timestamp() + .read_untracked() + .cmp( + &<ArcStore<filamento::chat::Message> as Clone>::clone( + &new_message.message, + ) + .timestamp() + .read_untracked(), + ) + }) { + Ok(i) => i, + Err(i) => i, + }; + messages.insert_before( + // TODO: check if this logic is correct + index, + <ArcStore<filamento::chat::Message> as Clone>::clone( + &new_message.message, + ) + .id() + .get_untracked(), + new_message, + ); + debug!("set the new message in message buffer"); + } } else { - let index = match messages.binary_search_by(|_, value| { - <ArcStore<filamento::chat::Message> as Clone>::clone(&value.message).timestamp().read_untracked().cmp(&<ArcStore<filamento::chat::Message> as Clone>::clone(&new_message.message).timestamp().read_untracked()) - }) { - Ok(i) => i, - Err(i) => i, - }; - messages.insert_before( - // TODO: check if this logic is correct - index, - <ArcStore<filamento::chat::Message> as Clone>::clone(&new_message.message).id().get_untracked(), + messages.insert( + <ArcStore<filamento::chat::Message> as Clone>::clone(&new_message.message) + .id() + .get_untracked(), new_message, ); debug!("set the new message in message buffer"); } - } else { - messages - .insert(<ArcStore<filamento::chat::Message> as Clone>::clone(&new_message.message).id().get_untracked(), 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()); + 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<JID> = None; - let mut messages = messages.get().into_iter().map(|(id, message)| { - let message_timestamp = <ArcStore<filamento::chat::Message> as Clone>::clone(&message.message).timestamp().read().naive_local(); - // 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(<ArcStore<filamento::chat::Message> as Clone>::clone(&message.message).from().get()); - last_timestamp = message_timestamp; - (id, (message, major, false)) - }).collect::<Vec<_>>(); + let mut messages = messages + .get() + .into_iter() + .map(|(id, message)| { + let message_timestamp = + <ArcStore<filamento::chat::Message> as Clone>::clone(&message.message) + .timestamp() + .read() + .naive_local(); + // 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( + <ArcStore<filamento::chat::Message> as Clone>::clone(&message.message) + .from() + .get(), + ); + last_timestamp = message_timestamp; + (id, (message, major, false)) + }) + .collect::<Vec<_>>(); if let Some((_id, (_, _, last))) = messages.last_mut() { *last = true } @@ -858,23 +974,40 @@ pub fn IconComponent(icon: Icon) -> impl IntoView { pub fn Delivery(delivery: Delivery) -> impl IntoView { match delivery { // TODO: proper icon coloring/theming - Delivery::Sending => view! { <IconComponent class:visible=true class:light=true icon=Icon::Sending16 /> }.into_any(), - Delivery::Written => view! { <IconComponent class:light=true icon=Icon::Sent16 /> }.into_any(), + Delivery::Sending => { + view! { <IconComponent class:visible=true class:light=true icon=Icon::Sending16 /> } + .into_any() + } + Delivery::Written => { + view! { <IconComponent class:light=true icon=Icon::Sent16 /> }.into_any() + } // TODO: message receipts // Delivery::Written => view! {}.into_any(), Delivery::Sent => view! { <IconComponent class:light=true icon=Icon::Sent16 /> }.into_any(), - Delivery::Delivered => view! { <IconComponent class:light=true icon=Icon::Delivered16 /> }.into_any(), + Delivery::Delivered => { + view! { <IconComponent class:light=true icon=Icon::Delivered16 /> }.into_any() + } // TODO: check if there is also the icon class - Delivery::Read => view! { <IconComponent class:light=true class:read=true icon=Icon::Delivered16 /> }.into_any(), - Delivery::Failed => view! { <IconComponent class:visible=true class:light=true icon=Icon::Error16Color /> }.into_any(), + Delivery::Read => { + view! { <IconComponent class:light=true class:read=true icon=Icon::Delivered16 /> } + .into_any() + } + Delivery::Failed => { + view! { <IconComponent class:visible=true class:light=true icon=Icon::Error16Color /> } + .into_any() + } // TODO: queued icon - Delivery::Queued => view! { <IconComponent class:visible=true class:light=true icon=Icon::Sending16 /> }.into_any(), - } + Delivery::Queued => { + view! { <IconComponent class:visible=true class:light=true icon=Icon::Sending16 /> } + .into_any() + } + } } #[component] pub fn Message(message: MacawMessage, major: bool, r#final: bool) -> impl IntoView { - let message_message: Store<Message> = <ArcStore<filamento::chat::Message> as Clone>::clone(&message.message).into(); + let message_message: Store<Message> = + <ArcStore<filamento::chat::Message> as Clone>::clone(&message.message).into(); let message_user = <ArcStore<filamento::user::User> as Clone>::clone(&message.user).into(); let avatar = LocalResource::new(move || get_avatar(message_user)); let name = move || get_name(message_user); @@ -927,13 +1060,28 @@ pub fn ChatViewMessageComposer(chat: JID) -> impl IntoView { let send_message = move || { let value = chat.clone(); - spawn_local(async move { match client.read_untracked().send_message(value, Body { body: new_message.get_untracked() }).await { - Ok(_) => { - new_message.set("".to_string()); - message_input.write_untracked().as_ref().expect("message input div not mounted").set_text_content(Some("")); - }, - Err(e) => tracing::error!("message send error: {}", e), - }}) + spawn_local(async move { + match client + .read_untracked() + .send_message( + value, + Body { + body: new_message.get_untracked(), + }, + ) + .await + { + Ok(_) => { + new_message.set("".to_string()); + message_input + .write_untracked() + .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 |_| { @@ -1005,7 +1153,7 @@ impl<K, V> PartialEq for ArcStateStore<K, V> { impl<K, V> Clone for ArcStateStore<K, V> { fn clone(&self) -> Self { Self { - store: Arc::clone(&self.store) + store: Arc::clone(&self.store), } } } @@ -1022,7 +1170,7 @@ impl<K, V> ArcStateStore<K, V> { #[derive(Debug)] struct StateStore<K, V, S = SyncStorage> { - inner: ArenaItem<ArcStateStore<K, V>, S> + inner: ArenaItem<ArcStateStore<K, V>, S>, } impl<K, V, S> Dispose for StateStore<K, V, S> { @@ -1031,13 +1179,22 @@ impl<K, V, S> Dispose for StateStore<K, V, S> { } } -impl<K, V> StateStore<K, V> where K: Send + Sync + 'static, V: Send + Sync + 'static { +impl<K, V> StateStore<K, V> +where + K: Send + Sync + 'static, + V: Send + Sync + 'static, +{ pub fn new() -> Self { Self::new_with_storage() } } -impl<K, V, S> StateStore<K, V, S> where K: 'static, V: 'static, S: Storage<ArcStateStore<K, V>> { +impl<K, V, S> StateStore<K, V, S> +where + K: 'static, + V: 'static, + S: Storage<ArcStateStore<K, V>>, +{ pub fn new_with_storage() -> Self { Self { inner: ArenaItem::new_with_storage(ArcStateStore::new()), @@ -1045,13 +1202,21 @@ impl<K, V, S> StateStore<K, V, S> where K: 'static, V: 'static, S: Storage<ArcSt } } -impl<K, V> StateStore<K, V, LocalStorage> where K: 'static, V: 'static { +impl<K, V> StateStore<K, V, LocalStorage> +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<ArcStateStore<K, V>> for StateStore<K, V> { +impl< + K: std::marker::Send + std::marker::Sync + 'static, + V: std::marker::Send + std::marker::Sync + 'static, +> From<ArcStateStore<K, V>> for StateStore<K, V> +{ fn from(value: ArcStateStore<K, V>) -> Self { Self { inner: ArenaItem::new_with_storage(value), @@ -1075,7 +1240,11 @@ impl<K, V, S> Clone for StateStore<K, V, S> { } } -impl<K: Eq + std::hash::Hash + Clone, V: Clone> StateStore<K, V> where K: Send + Sync + 'static, V: Send + Sync + 'static { +impl<K: Eq + std::hash::Hash + Clone, V: Clone> StateStore<K, V> +where + K: Send + Sync + 'static, + V: Send + Sync + 'static, +{ pub fn store(&self, key: K, value: V) -> StateListener<K, V> { { let store = self.inner.try_get_value().unwrap(); @@ -1091,12 +1260,19 @@ impl<K: Eq + std::hash::Hash + Clone, V: Clone> StateStore<K, V> where K: Send + }; StateListener { value, - cleaner: StateCleaner { key, state_store: self.clone() }, + cleaner: StateCleaner { + key, + state_store: self.clone(), + }, } } } -impl<K, V> StateStore<K, V> where K: Eq + std::hash::Hash + Send + Sync + 'static, V: Send + Sync + 'static { +impl<K, V> StateStore<K, V> +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(); @@ -1127,12 +1303,20 @@ impl<K, V> StateStore<K, V> where K: Eq + std::hash::Hash + Send + Sync + 'stati } #[derive(Clone)] -struct StateListener<K, V> where K: Eq + std::hash::Hash + 'static + std::marker::Send + std::marker::Sync, V: 'static + std::marker::Send + std::marker::Sync { +struct StateListener<K, V> +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<K, V> + cleaner: StateCleaner<K, V>, } -impl<K: std::cmp::Eq + std::hash::Hash + std::marker::Send + std::marker::Sync, V: std::marker::Send + std::marker::Sync> Deref for StateListener<K, V> { +impl< + K: std::cmp::Eq + std::hash::Hash + std::marker::Send + std::marker::Sync, + V: std::marker::Send + std::marker::Sync, +> Deref for StateListener<K, V> +{ type Target = V; fn deref(&self) -> &Self::Target { @@ -1140,7 +1324,9 @@ impl<K: std::cmp::Eq + std::hash::Hash + std::marker::Send + std::marker::Sync, } } -impl<K: std::cmp::Eq + std::hash::Hash + Send + Sync, V: Send + Sync> DerefMut for StateListener<K, V> { +impl<K: std::cmp::Eq + std::hash::Hash + Send + Sync, V: Send + Sync> DerefMut + for StateListener<K, V> +{ fn deref_mut(&mut self) -> &mut Self::Target { &mut self.value } @@ -1151,12 +1337,20 @@ struct ArcStateCleaner<K, V> { state_store: ArcStateStore<K, V>, } -struct StateCleaner<K, V> where K: Eq + std::hash::Hash + Send + Sync + 'static, V: Send + Sync + 'static { +struct StateCleaner<K, V> +where + K: Eq + std::hash::Hash + Send + Sync + 'static, + V: Send + Sync + 'static, +{ key: K, state_store: StateStore<K, V>, } -impl<K, V> Clone for StateCleaner<K, V> where K: Eq + std::hash::Hash + Clone + Send + Sync, V: Send + Sync { +impl<K, V> Clone for StateCleaner<K, V> +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(); @@ -1172,7 +1366,9 @@ impl<K, V> Clone for StateCleaner<K, V> where K: Eq + std::hash::Hash + Clone + } } -impl<K: Eq + std::hash::Hash + Send + Sync + 'static, V: Send + Sync + 'static> Drop for StateCleaner<K, V> { +impl<K: Eq + std::hash::Hash + Send + Sync + 'static, V: Send + Sync + 'static> Drop + for StateCleaner<K, V> +{ fn drop(&mut self) { self.state_store.remove(&self.key); } @@ -1186,14 +1382,13 @@ struct MacawChat { impl MacawChat { fn got_chat_and_user(chat: Chat, user: User) -> Self { - let chat_state_store: StateStore<JID, ArcStore<Chat>> = use_context().expect("no chat state store"); - let user_state_store: StateStore<JID, ArcStore<User>> = use_context().expect("no user state store"); + let chat_state_store: StateStore<JID, ArcStore<Chat>> = + use_context().expect("no chat state store"); + 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)); let chat = chat_state_store.store(chat.correspondent.clone(), ArcStore::new(chat)); - Self { - chat, - user, - } + Self { chat, user } } } @@ -1219,14 +1414,13 @@ struct MacawMessage { impl MacawMessage { fn got_message_and_user(message: Message, user: User) -> Self { - let message_state_store: StateStore<Uuid, ArcStore<Message>> = use_context().expect("no message state store"); - let user_state_store: StateStore<JID, ArcStore<User>> = use_context().expect("no user state store"); + let message_state_store: StateStore<Uuid, ArcStore<Message>> = + use_context().expect("no message state store"); + let user_state_store: StateStore<JID, ArcStore<User>> = + 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, - } + Self { message, user } } } @@ -1271,12 +1465,10 @@ struct MacawContact { impl MacawContact { fn got_contact_and_user(contact: Contact, user: User) -> Self { let contact = Store::new(contact); - let user_state_store: StateStore<JID, ArcStore<User>> = use_context().expect("no user state store"); + 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 { - contact, - user, - } + Self { contact, user } } } @@ -1300,17 +1492,29 @@ fn ChatsList() -> impl IntoView { let load_chats = LocalResource::new(move || async move { let client = use_context::<Client>().expect("client not in context"); - let chats = client.get_chats_ordered_with_latest_messages_and_users().await.map_err(|e| e.to_string()); + 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::<IndexMap<JID, _>>(); + 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::<IndexMap<JID, _>>(); set_chats.set(chats); - }, + } Err(_) => { // TODO: show error message at top of chats list - }, + } } }); @@ -1382,15 +1586,20 @@ fn RosterList() -> impl IntoView { #[component] 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 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> = use_context().expect("no open chats panel store in context"); + let open_chats: Store<OpenChatsPanel> = + 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 = Chat { + correspondent: contact_user.jid().get(), + have_chatted: false, + }; let chat = MacawChat::got_chat_and_user(chat, contact_user.get()); let open_chat = move |_| { @@ -1398,20 +1607,24 @@ fn RosterListItem(contact: MacawContact) -> impl IntoView { open_chats.update(|open_chats| open_chats.open(chat.clone())); }; - let open =move || { + 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 + return Open::Focused; } } - if let Some(_backgrounded_chat) = open_chats.chats().read().get(contact_user.jid().read().deref()) { - return Open::Open - } + 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(); + let focused = move || open().is_focused(); + let open = move || open().is_open(); view! { <div class="roster-list-item" class:open=move || open() class:focused=move || focused() on:click=open_chat> @@ -1487,33 +1700,39 @@ impl Open { #[component] 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 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 let latest_message_body = move || message.get().body.body; - let open_chats: Store<OpenChatsPanel> = use_context().expect("no open chats panel store in context"); + let open_chats: Store<OpenChatsPanel> = + 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 || { + 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 + return Open::Focused; } } - if let Some(_backgrounded_chat) = open_chats.chats().read().get(chat_chat.correspondent().read().deref()) { - return Open::Open - } + 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 focused = move || open().is_focused(); + let open = move || open().is_open(); view! { <div class="chats-list-item" class:open=move || open() class:focused=move || focused() on:click=open_chat> |