diff options
| -rw-r--r-- | rust-toolchain.toml | 2 | ||||
| -rw-r--r-- | src/lib.rs | 633 | 
2 files changed, 428 insertions, 207 deletions
diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..5d56faf --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" @@ -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>  | 
