summaryrefslogblamecommitdiffstats
path: root/src/lib.rs
blob: 11ebc54183cc46f66a606dbe6dc1b499879a2a9a (plain) (tree)





















                                                          
                       
             



                               
                            
                           

                           
                                         

                   





























                                                     
                                                                                          





                                                                                                

















                                                                                

                                                           


            
                                                                                                                                



                                                 
 
                                                        











                                                                                 
                                            





























                                                                                                              




                                      
 









                                                      


                                                    
                                            



































































                                                                                                
                            



                                                                               





                                                                             
 







                                                                   

                                                       
















                                                                                        












                                               


     
























































                                                                    

 



                                                                                

 





                                                                          

 



                                                                             

 



                                                                               

 










                                                                                                       

 





                                                                                             


                  

                                          



                                                          



                                                                                                                                 
              

                 




                          
                                                  













                                                  

                                                 



                                                                   



                                                                                                                                           
              

                    




                             
                                                      











                                                  


















                                                  
                                 

















                                                                                                                                                            


                                                                                              
                            
                                                              














                                                                                            
                 

             
                                      

       



                                      


                                                                                  


                  
 
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, spawn_local},
};
use leptos_meta::Stylesheet;
use reactive_stores::Store;
use stylance::import_style;
use thiserror::Error;
use tokio::sync::{mpsc::Receiver, Mutex};
use tracing::debug;
use uuid::Uuid;

pub enum AppState {
    LoggedOut,
    LoggedIn,
}

#[derive(Clone)]
pub struct Client {
    client: filamento::Client<FilesMem>,
    jid: Arc<JID>,
    file_store: FilesMem,
}

impl Deref for Client {
    type Target = filamento::Client<FilesMem>;

    fn deref(&self) -> &Self::Target {
        &self.client
    }
}

impl DerefMut for Client {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.client
    }
}

#[component]
pub fn App() -> impl IntoView {
    let (app, set_app) = signal(AppState::LoggedOut);
    let client: RwSignal<Option<(Client, Receiver<UpdateMessage>)>> = RwSignal::new(None);

    view! {
        {move || match &*app.read() {
            AppState::LoggedOut => view! { <LoginPage set_app set_client=client /> }.into_any(),
            AppState::LoggedIn => {
                if let Some((client, updates)) = client.write_untracked().take() {
                    view! { <Macaw client updates /> }.into_any()
                } else {
                    set_app.set(AppState::LoggedOut);
                    view! { <LoginPage set_app set_client=client /> }.into_any()
                }
            }
        }}
    }
}

#[derive(Clone, Debug, Error)]
pub enum LoginError {
    #[error("Missing Password")]
    MissingPassword,
    #[error("Missing JID")]
    MissingJID,
    #[error("Invalid JID: {0}")]
    InvalidJID(#[from] jid::ParseError),
    #[error("Connection Error: {0}")]
    ConnectionError(#[from] CommandError<ConnectionError>),
}

#[component]
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(None::<LoginError>);
    let error_message = move || {
        error.with(|error| {
            if let Some(error) = error {
                view! { <div class="error">{error.to_string()}</div> }.into_any()
            } else {
                view! {}.into_any()
            }
        })
    };

    let (login_pending, set_login_pending) = signal(false);

    let login = Action::new_local(move |_| {
        async move {
            set_login_pending.set(true);

            if jid.read_untracked().is_empty() {
                set_error.set(Some(LoginError::MissingJID));
                set_login_pending.set(false);
                return
            }

            if password.read_untracked().is_empty() {
                set_error.set(Some(LoginError::MissingPassword));
                set_login_pending.set(false);
                return
            }

            let jid = match JID::from_str(&jid.read_untracked()) {
                Ok(j) => j,
                Err(e) => {
                    set_error.set(Some(e.into()));
                    set_login_pending.set(false);
                    return
                },
            };

            // initialise the client
            let db = Db::create_connect_and_migrate("mem.db").await.unwrap();
            let files_mem = FilesMem::new();
            let (client, updates) =
                filamento::Client::new(jid.clone(), password.read_untracked().clone(), db, files_mem.clone());
            // TODO: remember_me
            let client = Client {
                client,
                jid: Arc::new(jid),
                file_store: files_mem,
            };

            if *connect_on_login.read_untracked() {
                match client.connect().await {
                    Ok(_) => {},
                    Err(e) => {
                        set_error.set(Some(e.into()));
                        set_login_pending.set(false);
                        return
                    },
                }
            }

            // debug!("before setting app state");
            set_client.set(Some((client, updates)));
            set_app.set(AppState::LoggedIn);
        }
    });

    view! {
        <div class="center fill">
            <div id="login-form" class="panel">
                <div id="hero">
                    <img src="/assets/icon.png" />
                    <h1>Macaw Instant Messenger</h1>
                </div>
                {error_message}
                <form on:submit=move |ev| {
                    ev.prevent_default();
                    login.dispatch(());
                }>
                    <label for="jid">JID</label>
                    <input
                        disabled=login_pending
                        placeholder="caw@macaw.chat"
                        type="text"
                        bind:value=jid
                        name="jid"
                        id="jid"
                        autofocus="true"
                    />
                    <label for="password">Password</label>
                    <input
                        disabled=login_pending
                        placeholder="••••••••"
                        type="password"
                        bind:value=password
                        name="password"
                        id="password"
                    />
                    <div>
                        <label for="remember_me">Remember me</label>
                        <input
                            disabled=login_pending
                            type="checkbox"
                            bind:checked=remember_me
                            name="remember_me"
                            id="remember_me"
                        />
                    </div>
                    <div>
                        <label for="connect_on_login">Connect on login</label>
                        <input
                            disabled=login_pending
                            type="checkbox"
                            bind:checked=connect_on_login
                            name="connect_on_login"
                            id="connect_on_login"
                        />
                    </div>
                    <input disabled=login_pending class="button" type="submit" value="Log In" />
                </form>
            </div>
        </div>
    }
}

#[component]
fn Macaw(
    // TODO: logout
    // app_state: WriteSignal<Option<essage>)>, LocalStorage>,
    client: Client,
    mut updates: Receiver<UpdateMessage>,
) -> impl IntoView {
    provide_context(client);

    let (new_messages, set_new_messages) = signal(None::<(JID, MacawMessage)>);
    provide_context(new_messages);

    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));

    // // 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) => {},
                UpdateMessage::Offline(offline) => {},
                UpdateMessage::RosterUpdate(contact, user) => {},
                UpdateMessage::RosterDelete(jid) => {},
                UpdateMessage::Presence { from, presence } => {},
                UpdateMessage::Message { to, from, message } => {
                    let new_message = MacawMessage::got_message_and_user(message, from);
                    set_new_messages.set(Some((to, new_message)));
                },
                UpdateMessage::MessageDelivery { id, chat, delivery } => {},
                UpdateMessage::SubscriptionRequest(jid) => {},
                UpdateMessage::NickChanged { jid, nick } => {},
                UpdateMessage::AvatarChanged { jid, id } => {},
            }
        }
    });

    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(),
        }
    }
}

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);
            }
        }
    }
}

#[derive(Clone)]
struct StateListener<K, V> where K: Eq + std::hash::Hash + 'static, V: 'static {
    value: V,
    cleaner: StateCleaner<K, V>
}

impl<K: std::cmp::Eq + std::hash::Hash, V> Deref for StateListener<K, V> {
    type Target = V;

    fn deref(&self) -> &Self::Target {
        &self.value
    }
}

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
    }
}

struct StateCleaner<K, V> where K: Eq + std::hash::Hash + 'static, V: 'static {
    key: K,
    _ty: PhantomData<V>,
    // state_store: StateStore<K, V>,
}

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,
        }
    }
}

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: StateListener<JID, Store<Chat>>,
    user: StateListener<JID, Store<User>>,
}

impl MacawChat {
    fn got_chat_and_user(chat: Chat, user: User) -> Self {
        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,
            user,
        }
    }
}

impl Deref for MacawChat {
    type Target = StateListener<JID, Store<Chat>>;

    fn deref(&self) -> &Self::Target {
        &self.chat
    }
}

impl DerefMut for MacawChat {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.chat
    }
}

#[derive(Clone)]
struct MacawMessage {
    message: StateListener<Uuid, Store<Message>>,
    user: StateListener<JID, Store<User>>,
}

impl MacawMessage {
    fn got_message_and_user(message: Message, user: User) -> Self {
        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,
            user,
        }
    }
}

impl Deref for MacawMessage {
    type Target = StateListener<Uuid, Store<Message>>;

    fn deref(&self) -> &Self::Target {
        &self.message
    }
}

impl DerefMut for MacawMessage {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.message
    }
}

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 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();
    spawn_local(async move {
        let mut new_message = new_messages_signal.to_stream();
        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");
    });

    view! {
        <div class="chats-list panel">
            <h2>Chats</h2>
            <div>
                <For each=move || chats.get() key=|chat| chat.0.clone() let(chat)>
                    <p>{chat.0.to_string()}</p>
                </For>
            </div>
        </div>
    }
}