diff options
author | 2025-04-29 20:58:02 +0100 | |
---|---|---|
committer | 2025-04-29 20:58:02 +0100 | |
commit | 22141aa6d7b0f97441a60111046d1ef7260b68dc (patch) | |
tree | 68667672a2777aec2cfcf36575b973fb3e64e7f8 /src/lib.rs | |
parent | 0e902e1f0a56e2f59cb91065a0ad8600631a1e49 (diff) | |
download | macaw-web-22141aa6d7b0f97441a60111046d1ef7260b68dc.tar.gz macaw-web-22141aa6d7b0f97441a60111046d1ef7260b68dc.tar.bz2 macaw-web-22141aa6d7b0f97441a60111046d1ef7260b68dc.zip |
fix: use_context works
Diffstat (limited to 'src/lib.rs')
-rw-r--r-- | src/lib.rs | 363 |
1 files changed, 226 insertions, 137 deletions
@@ -1,16 +1,36 @@ -use std::{ops::{Deref, DerefMut}, str::FromStr, sync::Arc, thread::sleep, time::{self, Duration}}; - -use filamento::{chat::{Chat, Message}, db::Db, error::{CommandError, ConnectionError, DatabaseError}, files::FilesMem, user::User, UpdateMessage}; +use std::{ + borrow::Borrow, + cell::RefCell, + collections::HashMap, + marker::PhantomData, + ops::{Deref, DerefMut}, + rc::Rc, + str::FromStr, + sync::{atomic::AtomicUsize, Arc}, + thread::sleep, + time::{self, Duration}, +}; + +use filamento::{ + chat::{Chat, Message}, + db::Db, + error::{CommandError, ConnectionError, DatabaseError}, + files::FilesMem, + user::User, + UpdateMessage, +}; +use futures::stream::StreamExt; use indexmap::IndexMap; use jid::JID; -use leptos::{prelude::*, task::spawn_local}; -use futures::stream::StreamExt; +use leptos::{ + prelude::*, + task::{spawn, spawn_local}, +}; use leptos_meta::Stylesheet; -use leptos_query::{create_query, provide_query_client, provide_query_client_with_options, DefaultQueryOptions, QueryOptions, QueryResult, QueryScope}; -use leptos_reactive::{SignalGetUntracked, SignalStream}; +use reactive_stores::Store; use stylance::import_style; use thiserror::Error; -use tokio::{sync::mpsc::Receiver}; +use tokio::sync::{mpsc::Receiver, Mutex}; use tracing::debug; use uuid::Uuid; @@ -43,7 +63,7 @@ impl DerefMut for Client { #[component] pub fn App() -> impl IntoView { let (app, set_app) = signal(AppState::LoggedOut); - let client: RwSignal<Option<(Client, Receiver<UpdateMessage>)>, LocalStorage> = RwSignal::new_local(None); + let client: RwSignal<Option<(Client, Receiver<UpdateMessage>)>> = RwSignal::new(None); view! { {move || match &*app.read() { @@ -73,13 +93,13 @@ pub enum LoginError { } #[component] -fn LoginPage(set_app: WriteSignal<AppState>, set_client: RwSignal<Option<(Client, Receiver<UpdateMessage>)>, LocalStorage>) -> 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); let connect_on_login = RwSignal::new(true); - let (error, set_error) = signal_local(None::<LoginError>); + let (error, set_error) = signal(None::<LoginError>); let error_message = move || { error.with(|error| { if let Some(error) = error { @@ -128,7 +148,7 @@ fn LoginPage(set_app: WriteSignal<AppState>, set_client: RwSignal<Option<(Client jid: Arc::new(jid), file_store: files_mem, }; - // TODO: connect on login + if *connect_on_login.read_untracked() { match client.connect().await { Ok(_) => {}, @@ -211,19 +231,26 @@ fn Macaw( client: Client, mut updates: Receiver<UpdateMessage>, ) -> impl IntoView { - // TODO: is there some kind of context_local? - let client = RwSignal::new_local(client); provide_context(client); let (new_messages, set_new_messages) = signal(None::<(JID, MacawMessage)>); provide_context(new_messages); - provide_query_client_with_options(DefaultQueryOptions { - resource_option: leptos_query::ResourceOption::Local, - ..Default::default() - }); + let messages_store: StateStore<Uuid, Store<Message>> = StateStore::new(); + provide_context(RwSignal::new_local(messages_store)); + let chats_store: StateStore<JID, Store<Chat>> = StateStore::new(); + provide_context(RwSignal::new_local(chats_store)); + let users_store: StateStore<JID, Store<User>> = StateStore::new(); + provide_context(RwSignal::new_local(users_store)); - let updates_routine = OnceResource::new(async move { + // // here we create a signal in the root that can be consumed + // // anywhere in the app. + // let (count, set_count) = signal(0); + // // we'll pass the setter to specific components, + // // but provide the count itself to the whole app via context + // provide_context(count); + + OnceResource::new(async move { while let Some(update) = updates.recv().await { match update { UpdateMessage::Online(online, items) => {}, @@ -243,85 +270,148 @@ fn Macaw( } }); - view! { - "logged in" - <ChatsList /> + view! { <ChatsList /> } +} + +// V has to be an arc signal +struct StateStore<K, V> { + store: Rc<RefCell<HashMap<K, (V, usize)>>>, +} + +impl<K, V> Clone for StateStore<K, V> { + fn clone(&self) -> Self { + Self { + store: self.store.clone(), + } } } -fn chat_query() -> QueryScope<JID, Result<Chat, String>> { - create_query(get_chat, QueryOptions::default()) +impl<K, V> StateStore<K, V> { + pub fn new() -> Self { + Self { + store: Rc::new(RefCell::new(HashMap::new())), + } + } +} + +impl<K: Eq + std::hash::Hash + Clone, V: Clone> StateStore<K, V> { + pub fn store(&self, key: K, value: V) -> StateListener<K, V> { + let mut store = self.store.borrow_mut(); + if let Some((v, count)) = store.get_mut(&key) { + *v = value.clone(); + *count += 1; + StateListener { + value, + cleaner: StateCleaner { key, _ty: PhantomData }, + } + } else { + store.insert(key.clone(), (value.clone(), 1)); + StateListener { + value, + cleaner: StateCleaner { + key, + _ty: PhantomData, + } + } + } + } + + pub fn init() { + + } +} + +impl<K: Eq + std::hash::Hash , V> StateStore<K, V> { + pub fn update(&self, key: &K, value: V) { + if let Some((v, _)) = self.store.borrow_mut().get_mut(key) { + *v = value; + } + } + + pub fn modify(&self, key: &K, modify: impl Fn(&mut V)) { + if let Some((v, _)) = self.store.borrow_mut().get_mut(key) { + modify(v); + } + } + + fn remove(&self, key: &K) { + let mut store = self.store.borrow_mut(); + if let Some((_v, count)) = store.get_mut(key) { + *count -= 1; + if *count == 0 { + store.remove(key); + } + } + } } -async fn get_chat(jid: JID) -> Result<Chat, String> { - let client: Client = use_context::<RwSignal<Client, LocalStorage>>().unwrap().read_untracked().clone(); - client.get_chat(jid).await.map_err(|e| e.to_string()) +#[derive(Clone)] +struct StateListener<K, V> where K: Eq + std::hash::Hash + 'static, V: 'static { + value: V, + cleaner: StateCleaner<K, V> } -fn user_query() -> QueryScope<JID, Result<User, String>> { - create_query(get_user, QueryOptions::default()) +impl<K: std::cmp::Eq + std::hash::Hash, V> Deref for StateListener<K, V> { + type Target = V; + + fn deref(&self) -> &Self::Target { + &self.value + } } -async fn get_user(jid: JID) -> Result<User, String> { - let client: Client = use_context::<RwSignal<Client, LocalStorage>>().unwrap().read_untracked().clone(); - client.get_user(jid).await.map_err(|e| e.to_string()) +impl<K: std::cmp::Eq + std::hash::Hash, V> DerefMut for StateListener<K, V> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.value + } } -fn message_query() -> QueryScope<Uuid, Result<Message, String>> { - create_query(get_message, QueryOptions::default()) +struct StateCleaner<K, V> where K: Eq + std::hash::Hash + 'static, V: 'static { + key: K, + _ty: PhantomData<V>, + // state_store: StateStore<K, V>, } -async fn get_message(id: Uuid) -> Result<Message, String> { - let client: Client = use_context::<RwSignal<Client, LocalStorage>>().unwrap().read_untracked().clone(); - client.get_message(id).await.map_err(|e| e.to_string()) +impl<K, V> Clone for StateCleaner<K, V> where K: Eq + std::hash::Hash + Clone { + fn clone(&self) -> Self { + let state_store = use_context::<RwSignal<StateStore<K, V>, LocalStorage>>().unwrap(); + if let Some((_v, count)) = state_store.read_untracked().store.borrow_mut().get_mut(&self.key) { + *count += 1; + } + Self { + key: self.key.clone(), + _ty: PhantomData, + } + } } -// fn got_chat(chat: Chat) -> QueryScope<JID, MacawChat> { -// let fetcher = move |_| { -// let chat = (&chat).clone(); -// async { -// MacawChat { -// chat -// } -// }}; -// create_query(fetcher, QueryOptions::default()) -// } +impl<K: Eq + std::hash::Hash + 'static, V: 'static> Drop for StateCleaner<K, V> { + fn drop(&mut self) { + let state_store = use_context::<RwSignal<StateStore<K, V>, LocalStorage>>().unwrap(); + state_store.read_untracked().remove(&self.key) + } +} #[derive(Clone)] struct MacawChat { - chat: ReadSignal<Option<Option<Result<Chat, String>>>>, - user: ReadSignal<Option<Option<Result<User, String>>>>, + chat: StateListener<JID, Store<Chat>>, + user: StateListener<JID, Store<User>>, } impl MacawChat { fn got_chat_and_user(chat: Chat, user: User) -> Self { - let correspondent = chat.correspondent.clone(); - let chat_query = chat_query(); - chat_query.set_query_data(correspondent.clone(), Ok(chat)); - let chat = chat_query.use_query(move || correspondent.clone()); - let jid = user.jid.clone(); - let user_query = user_query(); - user_query.set_query_data(jid.clone(), Ok(user)); - let user = user_query.use_query(move || jid.clone()); + let chat_state_store: RwSignal<StateStore<JID, Store<Chat>>, LocalStorage> = use_context().expect("no chat state store"); + let user_state_store: RwSignal<StateStore<JID, Store<User>>, LocalStorage> = use_context().expect("no user state store"); + let user = user_state_store.read_untracked().store(user.jid.clone(), Store::new(user)); + let chat = chat_state_store.read_untracked().store(chat.correspondent.clone(), Store::new(chat)); Self { - chat: ReadSignal::from_stream_unsync(chat.data.to_stream()), - user: ReadSignal::from_stream_unsync(user.data.to_stream()), - } - } - - fn get(jid: JID) -> Self { - let jid1 = jid.clone(); - let chat = chat_query().use_query(move || (&jid1).clone()); - let user = user_query().use_query(move || (&jid).clone()); - Self { - chat: ReadSignal::from_stream_unsync(chat.data.to_stream()), - user: ReadSignal::from_stream_unsync(user.data.to_stream()), + chat, + user, } } } impl Deref for MacawChat { - type Target = ReadSignal<Option<Option<Result<Chat, String>>>>; + type Target = StateListener<JID, Store<Chat>>; fn deref(&self) -> &Self::Target { &self.chat @@ -336,30 +426,25 @@ impl DerefMut for MacawChat { #[derive(Clone)] struct MacawMessage { - message: ReadSignal<Option<Option<Result<Message, String>>>>, - user: ReadSignal<Option<Option<Result<User, String>>>>, + message: StateListener<Uuid, Store<Message>>, + user: StateListener<JID, Store<User>>, } impl MacawMessage { fn got_message_and_user(message: Message, user: User) -> Self { - debug!("executing the got message"); - let message_id = message.id; - let message_query = message_query(); - message_query.set_query_data(message.id, Ok(message)); - let message = message_query.use_query(move || message_id); - let jid = user.jid.clone(); - let user_query = user_query(); - user_query.set_query_data(jid.clone(), Ok(user)); - let user = user_query.use_query(move || jid.clone()); + let message_state_store: RwSignal<StateStore<Uuid, Store<Message>>, LocalStorage> = use_context().expect("no message state store"); + let user_state_store: RwSignal<StateStore<JID, Store<User>>, LocalStorage> = use_context().expect("no user state store"); + let message = message_state_store.read_untracked().store(message.id, Store::new(message)); + let user = user_state_store.read_untracked().store(user.jid.clone(), Store::new(user)); Self { - message: ReadSignal::from_stream_unsync(message.data.to_stream()), - user: ReadSignal::from_stream_unsync(user.data.to_stream()), + message, + user, } } } impl Deref for MacawMessage { - type Target = ReadSignal<Option<Option<Result<Message, String>>>>; + type Target = StateListener<Uuid, Store<Message>>; fn deref(&self) -> &Self::Target { &self.message @@ -372,73 +457,77 @@ impl DerefMut for MacawMessage { } } +struct MacawUser { + user: StateListener<JID, Store<User>>, +} + +impl Deref for MacawUser { + type Target = StateListener<JID, Store<User>>; + + fn deref(&self) -> &Self::Target { + &self.user + } +} + +impl DerefMut for MacawUser { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.user + } +} + +#[component] fn ChatsList() -> impl IntoView { - let chats: LocalResource<std::result::Result<(ReadSignal<IndexMap<JID, (MacawChat, MacawMessage)>, LocalStorage>, WriteSignal<IndexMap<JID, (MacawChat, MacawMessage)>, LocalStorage>), String>> = LocalResource::new(move || async move || -> Result<_, _> { - let client: Client = use_context::<RwSignal<Client, LocalStorage>>().unwrap().read().clone(); - let chats = client.get_chats_ordered_with_latest_messages_and_users().await.map_err(|e| e.to_string())?; - let chats = chats.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, set_chats) = signal_local(chats); - Ok((chats, set_chats)) - }()); + let client = use_context::<Client>().expect("client not in context"); + let (chats, set_chats) = signal(IndexMap::new()); + + 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()); + 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, _>>(); + set_chats.set(chats); + }, + Err(_) => { + // TODO: show error message at top of chats list + }, + } + }); // TODO: filter new messages signal let new_messages_signal: ReadSignal<Option<(JID, MacawMessage)>> = use_context().unwrap(); - OnceResource::new(async move { + spawn_local(async move { let mut new_message = new_messages_signal.to_stream(); - match chats.await { - Ok((c, set_c)) => { - while let Some(new_message) = new_message.next().await { - debug!("got new message in let"); - if let Some((to, new_message)) = new_message { - if let Some((chat, _latest_message)) = set_c.write().shift_remove(&to) { - debug!("chat existed"); - set_c.write().insert_before(0, to, (chat, new_message)); - debug!("done setting"); - } else { - debug!("the chat didn't exist"); - let chat = MacawChat::get(to.clone()); - set_c.write().insert_before(0, to, (chat, new_message)); - debug!("done setting"); - } - } + load_chats.await; + while let Some(new_message) = new_message.next().await { + debug!("got new message in let"); + if let Some((to, new_message)) = new_message { + if let Some((chat, _latest_message)) = set_chats.write().shift_remove(&to) { + debug!("chat existed"); + set_chats.write().insert_before(0, to, (chat, new_message)); + debug!("done setting"); + } else { + debug!("the chat didn't exist"); + let chat = client.get_chat(to.clone()).await.unwrap(); + let user = client.get_user(to.clone()).await.unwrap(); + let chat = MacawChat::got_chat_and_user(chat, user); + set_chats.write().insert_before(0, to, (chat, new_message)); + debug!("done setting"); } - debug!("set the new message"); } - Err(_) => {}, } + debug!("set the new message"); }); view! { <div class="chats-list panel"> <h2>Chats</h2> <div> - {move || { - if let Some(chats) = &*chats.read() { - match &**chats { - Ok((chats, _)) => { - let chats = chats.clone(); - view! { - <For - each=move || chats.get() - key=|chat| chat.0.clone() - let(chat) - > - <p>{chat.0.to_string()}</p> - </For> - } - .into_any() - } - Err(e) => { - view! { <div class="error">{format!("{}", e)}</div> }.into_any() - } - } - } else { - None::<String>.into_any() - } - }} + <For each=move || chats.get() key=|chat| chat.0.clone() let(chat)> + <p>{chat.0.to_string()}</p> + </For> </div> </div> } |