summaryrefslogblamecommitdiffstats
path: root/src/lib.rs
blob: 772ea60b1e2bc7150287efde44dab95411fe8f92 (plain) (tree)
1
2
3
4
5
6
7
8
9


                   
                                    



                           
                                             



                           
                                               
                                                   
                
                                                                                                                                                                                                                                                                                                                                                                                           

                               
                       
                        
             
                                                        

                                            
                                                                                    
                               
  
                                                   

                           



                           
                            
               
                                                                                                                                         
 

                                                






                   
                                     
                                          
                      


                      
                       














                                                                                  
                                                                             























                                                                                    
             


                                                                                


                       
                                           














                                                     
                                                                                 


                                     
                                                                                         
                                   

                                                                                      

                                                     
                                                                         













                                        

                                                           

                            


            

                                   
                                                                       
                    



                                                 
 
                                                        











                                                                                 
                                            





                                                            
                       




                                                                 
                       






                                                                  

                           

              
                                                          
                                    
                                     
                                              


                                                                         



                                                                      
                                        





                                                                           

                               



                                           





                                                           
                                
                                                            

                                 
                                           
                                             
                                  
              
 

                                                   


                                             


                                                      

                               

                 


                                                    
                                            






                                               
                                                        




















































                                                                                                
                                 

                                                                        




                           
                                



                                   
                                                                           
                               

                                       
                                                                    
                           
                           
                                       


                 


                                      


                                                             

                                               
                                                          
                               
                               
                                           


                     


                                             





                                        
                                                                                  
                                             


                                  

     
                                                                                       
                                             
                                
                                                                    
                                              
                









                                                       
                                                                     

                                                             
         


     
                       
                   

                                                   











                                     
              




                                          


                                                   




                                                                                               







                                                                                       

                                                            



                                                                                       



                                                                     



                                                                                   







                                                                 
                                              
                                                                                   



                                                                                           


                                                                       



                                                                                           



                                                         



                                                                                       


                                                     
                              



                                                    
 


                                                
 


        

                          

                                                             









                                                            
                                                                                    
                                                                
                             

                                             

                                                                     

























































































                                                                                                                                                              










                                                                       

 





                                                              
                                   
                    
                             
                            
 
                                           

                            

                                                                           
 
                                                                                
                                    
                                                                             
                                 
                                                                             
                                 
 

                                                           

                                                             
 


                                                          








                                                                                 
                                                              
                                                                                              
                                           
                                               
 

                                                                             
                                  

                                                       
                                                         








                                                                                  
                                                    
                 



                                                                                                               









                                                                                            
                 



                                                       
                 
                                                               
                                                  
                                                                                                  

                                                                                          

                            
                                                                     
                                                                 
                                                                                  
                                                                                                                


                         
                                                                 
                                                 
                                                                                        
                                                

                                             
                                    


                                                       
                                                
                 
                                                                          





                                                                                      


                                                                                 
                                                             



                                                                          

                                                                                            
                 



             
           

                        
                              




                                                         


     





                                     

                                                                                
                 

                                

                              
                    

                                    

             



                                


     
            
                                   
                                                                                                             
 



                                                                                            
                                                       
 
           



                                                     

                                       










                                                                                                                                                                                         

                                                     














                                                                      
                          










                                                                                                                                                                                      



                                                        
                      


                                      
                                      

                      
























                                                                                    


                                                         


                                                    

                                                 


                                                    
                                 
                 


                                       




              







                                                                                                        
                                           




                                                         





                                            
                                                           




                                   






                           
                                                                                            
                                                                
                                                                               


                                                                                              
                                 
                                               
                                                                                                                                                                               
















                                                            





                                                        

                                                                
                                     























































                                                                                                            
                                                                           




















                                                                                                                 



                                                               

                       



                                                               


                        




                                                                                



















                                                                                  
                                                                                                       
           





                                                 



              
                                            





















                                                                                                 



                                                                                                                                                                                   
                          
                                               



                                                                                               
                                                                                              











                                                                                               





                                                            

                                           













































































                                                                                         
           



















                                                                                                                                                            
     

 
            







































                                                                                                   

                                                                             










                                                           

























                                                                                                         









                                                                         


     

                                                         
                                                                                         
                                                 



                                                         
                                                 









                                                               











                                                                                 
                                                                         



















                                                                                          




                                                                                     
                                            






                                                                  
                                                                 






                                                                                           
                               

                                                                                             
                                   





                                                                                 
                                   






                                                                                                
                                       




                                                                                          
                                            











                                                                                 
                                   



                                                                        
                        


                                                                                                  
                                   



                                                                    


             

                                            


                                                                           

         


                                                    
                                                  








                                                                                          
                                     


















                                                                                          







                                                                
                                                                                            



                                                                                     

 








                      
            










                 


                     













                                                                             
                                                                 










                                                                           


                                                                                   











                                      
                                










                                     


                                         

         









                                         
                                  















                                            




                                                   
                                                                                                                                                            






                                                      






                                                                                                


                                                                                                    


                                                                                          
                                                      







                                                                                                   
                            




                                                                                                

 

                                                                                    

                                                                                      
                                                                                               
                                                                      
                                                     


                                                       
                                                                                                           

               
                                                                

                                                                                            
                                                                       

                                 
                                    
                                              

                                                                                                                                     




                                                                            
                                                                                                                                                                       



                    
                                                                

                                                                                            
                      
                                                                                                           
                                                                                                                                                      





                    
                                                                
                                                     




                                                                      
                                                           
 
                                
                                 

                                
                       


                              
                                                






                                                    
                                






                                                                       
      



                                                  

                                             





                                                            








                                                                         
                        
           

                                              
         

                                
                                                                                                         

                                      


                                                          
                                                       













                                                           
                   
                                             

               


                            








                                               

 
                                          

                             
                                           
         


     


                                        

                          
                                                         



         

                                          
                                             







                                               




                             




                                





                                    


                                                                     

         
 
 




                                         



                                
 




                                                       



                                                      


     















                                                                                                




                                                                



                                                                  








                                                              



                                          



         




                                                    
                                             


                                                        




                                                            


                                                        




                               








                                                           
     

 
                




                                                                              
             
                                

 




                                                                              




                                      

 


                                                                              


                                                  

 




                                     




                                                    
           
                                  

 




                                                  
                             





                                                                        


                                  
                                                  

         

 


                                                                                    
                        
                                           

     


                  

                                                 



                                                          
                                                                   
                                                        
                                                                   
                                                        

                                                                                           
                           



                          
                                                         













                                                  
                                                    
                                                 



                                                                   

                                                                      
                                                                   
                                                        

                                                                                    
                              



                             
                                                         











                                                  
                
                  
                                                 




                                     
                                                                   



                                                                                 


                          
                                                         











                                                  
                

                            
                                                 




                                                                   
                                                                   
                                                        
                                                                                 
                              
















                                                  
            
                                 



                                                                             



                                                               

                      










                                                                                          
                                                       
                                     
             

                                                                
             

         
 

                                                           
                                       
                                                                                     
                                            
                                                                    
                         

                                                                                     
                                                                       
                                             

                                                                            
                                                                        
                                       
                                                                        











                                                                                     

             
                                      
       




                                                                
 

                                      

















                                                                                                                                


                                                                                              
                      


                  
 
 






























                                                                                 
                                                               
                                                    
                                                               













                                                                  

                                                              






                                                    
                               




















































                                                                                                   

                                  
                                                                                                             
 
                                                                             
                                                                 



                                       



                                                                                                                                    








                                                         












                                                        








                                                                                                       











                                             

                                                                                                                               
































                                                                                 
                                     






                                                       
                               






































                                                                                        
                                                                        







                                                   
                                                                        



                                     

                                                          


         
                                                                              








































                                                                                                               
                                                                                                                                                                                                                                                               





























                                                                                                                                                                                                             


                                                           

                                                                                
                                                     
 

                                                                     


                                                                                          



                                                






                                                                      
                        


                                                                  
                                     

             






                                                    

                    

                                              


                                                                                                                   
                                                    
                                   
                                                                                                                                                          
                                                                                                       




                  
                                                      
                                               
                                                                             

                                                                     


                                 






                                       
                                                                  
                                                                             





                                                                          
                              
                   












                                                   


























                                                               

                                                                           
                                                                                     

                                                                             
                                                                                                       
                                                 

                                          
                                                                        

                                                                     




                                                                      
 
                        


                                                                  
                                     

             






                                                          

                    

                                              
 








                                                                        
           
                                                                                                                  
                                                 
                                   

                                                                                                                                                



                  
use std::{
    borrow::Borrow,
    cell::RefCell,
    collections::{HashMap, HashSet},
    marker::PhantomData,
    ops::{Deref, DerefMut},
    rc::Rc,
    str::FromStr,
    sync::{atomic::AtomicUsize, Arc, RwLock},
    thread::sleep,
    time::{self, Duration},
};

use base64::{Engine, prelude::BASE64_STANDARD};
use chrono::{Local, NaiveDateTime, TimeDelta, Utc};
use filamento::{
    chat::{Body, Chat, ChatStoreFields, Delivery, Message, MessageStoreFields}, db::Db, error::{AvatarPublishError, CommandError, ConnectionError, DatabaseError, SubscribeError}, files::{opfs::OPFSError, FileStore, FilesMem, FilesOPFS}, presence::{Offline, Online, Presence, PresenceType, Show}, roster::{Contact, ContactStoreFields}, user::{User, UserStoreFields}, UpdateMessage
};
use futures::stream::StreamExt;
use indexmap::IndexMap;
use jid::{JID, BareJID};
use leptos::{
    ev::{Event, KeyboardEvent, MouseEvent, SubmitEvent},
    html::{self, Div, Input, Pre, Textarea},
    prelude::*,
    tachys::{dom::document, reactive_graph::bind::GetValue, renderer::dom::Element},
    task::{spawn, spawn_local},
};
use reactive_stores::{ArcStore, Store, StoreField};
use stylance::import_style;
use thiserror::Error;
use tokio::sync::{
    Mutex,
    mpsc::{self, Receiver},
};
use tracing::{debug, error};
use uuid::Uuid;
use web_sys::{js_sys::Uint8Array, wasm_bindgen::{prelude::Closure, JsCast, UnwrapThrowExt}, FileReader, HtmlInputElement, ProgressEvent};

const NO_AVATAR: &str = "/assets/no-avatar.png";

pub enum AppState {
    LoggedOut,
    LoggedIn,
}

#[derive(Clone)]
pub struct Client {
    client: filamento::Client<Files>,
    resource: ArcRwSignal<Option<String>>,
    jid: Arc<BareJID>,
    file_store: Files,
}

#[derive(Clone, Debug)]
pub enum Files {
    Mem(FilesMem),
    Opfs(FilesOPFS),
}

impl FileStore for Files {
    type Err = OPFSError;

    async fn is_stored(&self, name: &str) -> Result<bool, Self::Err> {
        match self {
            Files::Mem(files_mem) => Ok(files_mem.is_stored(name).await.unwrap()),
            Files::Opfs(files_opfs) => Ok(files_opfs.is_stored(name).await?),
        }
    }

    async fn store(&self, name: &str, data: &[u8]) -> Result<(), Self::Err> {
        match self {
            Files::Mem(files_mem) => Ok(files_mem.store(name, data).await.unwrap()),
            Files::Opfs(files_opfs) => Ok(files_opfs.store(name, data).await?),
        }
    }

    async fn delete(&self, name: &str) -> Result<(), Self::Err> {
        match self {
            Files::Mem(files_mem) => Ok(files_mem.delete(name).await.unwrap()),
            Files::Opfs(files_opfs) => Ok(files_opfs.delete(name).await?),
        }
    }
}

impl Files {
    pub async fn get_src(&self, file_name: &str) -> Option<String> {
        match self {
            Files::Mem(files_mem) => {
                if let Some(data) = files_mem.get_file(file_name).await {
                    let data = BASE64_STANDARD.encode(data);
                    Some(format!("data:image/jpg;base64, {}", data))
                } else {
                    None
                }
            }
            Files::Opfs(files_opfs) => files_opfs.get_src(file_name).await.ok(),
        }
    }
}

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

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

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

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

    view! {
        {move || match &*app.read() {
            AppState::LoggedOut => view! { <LoginPage set_app set_client /> }.into_any(),
            AppState::LoggedIn => {
                if let Some((client, updates)) = set_client.write_untracked().take() {
                    view! { <Macaw client updates set_app /> }.into_any()
                } else {
                    set_app.set(AppState::LoggedOut);
                    view! { <LoginPage set_app set_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>),
    #[error("OPFS: {0}")]
    OPFS(#[from] OPFSError),
}

#[component]
fn LoginPage(
    set_app: WriteSignal<AppState>,
    set_client: WriteSignal<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;
                }
            };

            let remember_me = remember_me.get_untracked();
            // initialise the client
            let db = if remember_me {
                debug!("creating db in opfs");
                Db::create_connect_and_migrate(jid.as_bare().to_string())
                    .await
                    .unwrap()
            } else {
                debug!("creating db in memory");
                Db::create_connect_and_migrate_memory().await.unwrap()
            };
            let files = if remember_me {
                let opfs = FilesOPFS::new(jid.as_bare().to_string()).await;
                match opfs {
                    Ok(f) => Files::Opfs(f),
                    Err(e) => {
                        set_error.set(Some(e.into()));
                        set_login_pending.set(false);
                        return;
                    }
                }
            } else {
                Files::Mem(FilesMem::new())
            };
            let (client, updates) = filamento::Client::new(
                jid.clone(),
                password.read_untracked().clone(),
                db,
                files.clone(),
            );
            // TODO: remember_me
            let resource = ArcRwSignal::new(None::<String>);
            let client = Client {
                client,
                resource: resource.clone(),
                jid: Arc::new(jid.to_bare()),
                file_store: files,
            };

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

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

    view! {
        <div class="center fill">
            <div id="login-form" class="panel">
                <div id="hero">
                    <img src="/assets/macaw-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>
    }
}

pub struct MessageSubscriptions {
    all: HashMap<Uuid, mpsc::Sender<(BareJID, MacawMessage)>>,
    subset: HashMap<BareJID, HashMap<Uuid, mpsc::Sender<MacawMessage>>>,
}

impl MessageSubscriptions {
    pub fn new() -> Self {
        Self {
            all: HashMap::new(),
            subset: HashMap::new(),
        }
    }

    pub async fn broadcast(&mut self, to: BareJID, message: MacawMessage) {
        // subscriptions to all
        let mut removals = Vec::new();
        for (id, sender) in &self.all {
            match sender.send((to.clone(), message.clone())).await {
                Ok(_) => {}
                Err(_) => {
                    removals.push(*id);
                }
            }
        }
        for removal in removals {
            self.all.remove(&removal);
        }

        // subscriptions to specific chat
        if let Some(subscribers) = self.subset.get_mut(&to) {
            let mut removals = Vec::new();
            for (id, sender) in &*subscribers {
                match sender.send(message.clone()).await {
                    Ok(_) => {}
                    Err(_) => {
                        removals.push(*id);
                    }
                }
            }
            for removal in removals {
                subscribers.remove(&removal);
            }
            if subscribers.is_empty() {
                self.subset.remove(&to);
            }
        }
    }

    pub fn subscribe_all(&mut self) -> (Uuid, Receiver<(BareJID, MacawMessage)>) {
        let (send, recv) = mpsc::channel(10);
        let id = Uuid::new_v4();
        self.all.insert(id, send);
        (id, recv)
    }

    pub fn subscribe_chat(&mut self, chat: BareJID) -> (Uuid, Receiver<MacawMessage>) {
        let (send, recv) = mpsc::channel(10);
        let id = Uuid::new_v4();
        if let Some(chat_subscribers) = self.subset.get_mut(&chat) {
            chat_subscribers.insert(id, send);
        } else {
            let hash_map = HashMap::from([(id, send)]);
            self.subset.insert(chat, hash_map);
        }
        (id, recv)
    }

    pub fn unsubscribe_all(&mut self, sub_id: Uuid) {
        self.all.remove(&sub_id);
    }

    pub fn unsubscribe_chat(&mut self, sub_id: Uuid, chat: BareJID) {
        if let Some(chat_subs) = self.subset.get_mut(&chat) {
            chat_subs.remove(&sub_id);
        }
    }
}

#[derive(Store, Clone)]
pub struct Roster {
    #[store(key: BareJID = |(jid, _)| jid.clone())]
    contacts: HashMap<BareJID, MacawContact>,
}

impl Roster {
    pub fn new() -> Self {
        Self {
            contacts: HashMap::new(),
        }
    }
}

// TODO: multiple panels
// pub struct OpenChats {
//     panels:
// }

#[derive(Store, Default)]
pub struct OpenChatsPanel {
    // jid must be a chat in the chats map
    chat_view: Option<BareJID>,
    #[store(key: BareJID = |(jid, _)| jid.clone())]
    chats: IndexMap<BareJID, MacawChat>,
}

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);
            *open_chats.chat_view().write() = Some(new_jid);
        } else {
            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();
        open_chats.chats().write().insert(new_jid.clone(), chat);
        *open_chats.chat_view().write() = Some(new_jid);
    }
}

impl OpenChatsPanel {
    pub fn open(&mut self, chat: MacawChat) {
        if let Some(jid) = &mut self.chat_view {
            debug!("a chat was already open");
            if let Some((index, _jid, entry)) = self.chats.shift_remove_full(jid) {
                let new_jid = <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();
                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();
            self.chats.insert(new_jid.clone(), chat);
            *&mut self.chat_view = Some(new_jid);
        }
        debug!("opened chat");
    }

    // TODO:
    // pub fn open_in_new_tab_unfocused(&mut self) {

    // }

    // pub fn open_in_new_tab_focus(&mut self) {

    // }
}

#[derive(Store)]
pub struct UserPresences {
    #[store(key: BareJID = |(jid, _)| jid.clone())]
    user_presences: HashMap<BareJID, ArcRwSignal<Presences>>,
}

impl UserPresences {
    pub fn clear(&mut self) {
        for (_user, presences) in &mut self.user_presences {
            presences.set(Presences::new())
        }
    }

    // TODO: should be a bare jid
    pub fn get_user_presences(&mut self, user: &BareJID) -> ArcRwSignal<Presences> {
        if let Some(presences) = self.user_presences.get(user) {
            presences.clone()
        } else {
            let presences = Presences::new();
            let signal = ArcRwSignal::new(presences);
            self.user_presences.insert(user.clone(), signal.clone());
            signal
        }
    }
}

impl UserPresences {
    pub fn new() -> Self {
        Self {
            user_presences: HashMap::new(),
        }
    }
}

pub struct Presences {
    /// presences are sorted by time, first by type, then by last activity.
    presences: IndexMap<String, Presence>
}

impl Presences {
    pub fn new() -> Self {
        Self {
            presences: IndexMap::new(),
        }
    }

    /// gets the highest priority presence
    pub fn presence(&self) -> Option<(String, Presence)> {
        if let Some((resource, presence)) = self.presences.iter().filter(|(_resource, presence)| if let PresenceType::Online(online) = &presence.presence {
            online.show == Some(Show::DoNotDisturb)
        } else {
            false
        }).next() {
            return Some((resource.clone(), presence.clone()))
        }
        if let Some((resource, presence)) = self.presences.iter().filter(|(_resource, presence)| if let PresenceType::Online(online) = &presence.presence {
            online.show == Some(Show::Chat)
        } else {
            false
        }).next() {
            return Some((resource.clone(), presence.clone()))
        }
        if let Some((resource, presence)) = self.presences.iter().filter(|(_resource, presence)| if let PresenceType::Online(online) = &presence.presence {
            online.show == None
        } else {
            false
        }).next() {
            return Some((resource.clone(), presence.clone()))
        }
        if let Some((resource, presence)) = self.presences.iter().filter(|(_resource, presence)| if let PresenceType::Online(online) = &presence.presence {
            online.show == Some(Show::Away)
        } else {
            false
        }).next() {
            return Some((resource.clone(), presence.clone()))
        }
        if let Some((resource, presence)) = self.presences.iter().filter(|(_resource, presence)| if let PresenceType::Online(online) = &presence.presence {
            online.show == Some(Show::ExtendedAway)
        } else {
            false
        }).next() {
            return Some((resource.clone(), presence.clone()))
        }
        if let Some((resource, presence)) = self.presences.iter().filter(|(_resource, presence)| if let PresenceType::Offline(_offline) = &presence.presence {
            true
        } else {
            false
        }).next() {
            return Some((resource.clone(), presence.clone()))
        } else {
            None
        }
    }

    pub fn update_presence(&mut self, resource: String, presence: Presence) {
        let index = match self.presences.binary_search_by(|_, existing_presence| {
            presence.timestamp
                .cmp(
                    &existing_presence.timestamp
                )
        }) {
            Ok(i) => i,
            Err(i) => i,
        };
        self.presences.insert_before(
            // TODO: check if this logic is correct
            index,
            resource,
            presence,
        );
    }

    pub fn resource_presence(&mut self, resource: String) -> Presence {
        if let Some(presence) = self.presences.get(&resource) {
            presence.clone()
        } else {
            Presence {
                timestamp: Utc::now(),
                presence: PresenceType::Offline(Offline::default()),
            }
        }
    }
}

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

    let roster = Store::new(Roster::new());
    provide_context(roster);

    let message_subscriptions = RwSignal::new(MessageSubscriptions::new());
    provide_context(message_subscriptions);

    let messages_store: StateStore<Uuid, ArcStore<Message>> = StateStore::new();
    provide_context(messages_store);
    let chats_store: StateStore<BareJID, ArcStore<Chat>> = StateStore::new();
    provide_context(chats_store);
    let users_store: StateStore<BareJID, ArcStore<User>> = StateStore::new();
    provide_context(users_store);

    let open_chats = Store::new(OpenChatsPanel::default());
    provide_context(open_chats);
    let show_settings  = RwSignal::new(None::<SettingsPage>);
    provide_context(show_settings);

    let user_presences = Store::new(UserPresences::new());
    provide_context(user_presences);

    let client_user = LocalResource::new(move || {
        async move {
            let client = use_context::<Client>().expect("client not in context");
            let user = client.get_user((*client.jid).clone()).await.unwrap();
            MacawUser::got_user(user)
        }
    });
    provide_context(client_user);

    // TODO: timestamp incoming/outgoing subscription requests
    let (subscription_requests, set_subscription_requests)= signal(HashSet::<BareJID>::new());
    provide_context(subscription_requests);
    provide_context(set_subscription_requests);

    // TODO: get cached contacts on login before getting the updated contacts

    OnceResource::new(async move {
        while let Some(update) = updates.recv().await {
            match update {
                UpdateMessage::Online(online, items) => {
                    let contacts = items
                        .into_iter()
                        .map(|(contact, user)| {
                            (
                                contact.user_jid.clone(),
                                MacawContact::got_contact_and_user(contact, user),
                            )
                        })
                        .collect();
                    roster.contacts().set(contacts);
                }
                UpdateMessage::Offline(offline) => {
                    // when offline, will no longer receive updated user presences, consider everybody offline.
                    user_presences.write().clear();
                }
                UpdateMessage::RosterUpdate(contact, user) => {
                    roster.contacts().update(|roster| {
                        if let Some(macaw_contact) = roster.get_mut(&contact.user_jid) {
                            macaw_contact.set(contact);
                        } else {
                            let jid = contact.user_jid.clone();
                            let contact = MacawContact::got_contact_and_user(contact, user);
                            roster.insert(jid, contact);
                        }
                    });
                }
                UpdateMessage::RosterDelete(jid) => {
                    roster.contacts().update(|roster| {
                        roster.remove(&jid);
                    });
                }
                UpdateMessage::Presence { from, presence } => {
                    let bare_jid = from.to_bare();
                    if let Some(presences) = user_presences.read().user_presences.get(&bare_jid) {
                        if let Some(resource) = from.resourcepart() {
                            presences.write().update_presence(resource.clone(), presence);
                        }
                    } else {
                        if let Some(resource) = from.resourcepart() {
                            let mut presences = Presences::new();
                            presences.update_presence(resource.clone(), presence);
                            user_presences.write().user_presences.insert(bare_jid, ArcRwSignal::new(presences));
                        }
                    }
                }
                UpdateMessage::Message { to, from, message } => {
                    debug!("before got message");
                    let new_message = MacawMessage::got_message_and_user(message, from);
                    debug!("after got message");
                    spawn_local(async move {
                        message_subscriptions
                            .write()
                            .broadcast(to, new_message)
                            .await
                    });
                    debug!("after set message");
                }
                UpdateMessage::MessageDelivery { id, chat, delivery } => {
                    messages_store.modify(&id, |message| {
                        <ArcStore<filamento::chat::Message> as Clone>::clone(&message)
                            .delivery()
                            .set(Some(delivery))
                    });
                }
                UpdateMessage::SubscriptionRequest(jid) => {
                    set_subscription_requests.update(|req| { req.insert(jid); });
                }
                UpdateMessage::NickChanged { jid, nick } => {
                    users_store.modify(&jid, |user| {
                        user.update(|user| *&mut user.nick = nick.clone())
                    });
                }
                UpdateMessage::AvatarChanged { jid, id } => {
                    users_store.modify(&jid, |user| *&mut user.write().avatar = id.clone());
                }
            }
        }
    });

    view! {
        <Sidebar />
        // <ChatsList />
        <OpenChatsPanelView />
        {move || if let Some(_) = *show_settings.read() {
            view! { <Settings /> }.into_any()
        } else {
            view! {}.into_any()
        }}
    }
}

#[derive(PartialEq, Eq, Clone, Copy)]
pub enum SidebarOpen {
    Roster,
    Chats,
}

/// returns whether the state was changed to open (true) or closed (false)
pub fn toggle_open(state: &mut Option<SidebarOpen>, open: SidebarOpen) -> bool {
    match state {
        Some(opened) => {
            if *opened == open {
                *state = None;
                false
            } else {
                *state = Some(open);
                true
            }
        }
        None => {
          *state = Some(open);  
          true
        },
    }
}

#[component]
pub fn Sidebar() -> impl IntoView {
    let requests: ReadSignal<HashSet<BareJID>> = use_context().expect("no pending subscriptions in context");

    // for what has been clicked open (in the background)
    let (open, set_open) = signal(None::<SidebarOpen>);
    // for what is just in the hovered state (not clicked to be pinned open yet necessarily)
    let (hovered, set_hovered) = signal(None::<SidebarOpen>);
    let (just_closed, set_just_closed) = signal(false);

    view! {
        <div class="sidebar" on:mouseleave=move |_| {
            set_hovered.set(None);
            set_just_closed.set(false);
        }>
            <div class="dock panel">
                <div class="shortcuts">
                    <div class="roster-tab dock-item" class:focused=move || *open.read() == Some(SidebarOpen::Roster) class:hovering=move || *hovered.read() == Some(SidebarOpen::Roster)
                    on:mouseenter=move |_| {
                        set_just_closed.set(false);
                        set_hovered.set(Some(SidebarOpen::Roster))
                    }
                    on:click=move |_| {
                        set_open.update(|state| {
                            if !toggle_open(state, SidebarOpen::Roster) {
                                set_just_closed.set(true);
                            }                             
                        })
                    }>
                        <div class="dock-pill"></div>
                        <div class="dock-icon">
                            <div class="icon-with-badge">
                            <img src="/assets/caw.png" />
                            {move || {
                                let len = requests.read().len();
                                if len > 0 {
                                    view! {
                                        <div class="badge">{len}</div>
                                    }.into_any()
                                } else {
                                    view! {}.into_any()
                                }
                            }}
                            </div>
                        </div>
                    </div>
                    <div class="chats-tab dock-item" class:focused=move || *open.read() == Some(SidebarOpen::Chats) class:hovering=move || *hovered.read() == Some(SidebarOpen::Chats)
                    on:mouseenter=move |_| {
                        set_just_closed.set(false);
                        set_hovered.set(Some(SidebarOpen::Chats))
                    }
                    on:click=move |_| {
                        set_open.update(|state| {
                            if !toggle_open(state, SidebarOpen::Chats) {
                                set_just_closed.set(true);
                            }                             
                        })
                    }>
                        <div class="dock-pill"></div>
                        <img src="/assets/bubble.png" />
                    </div>
                </div>
                <div class="pins">
                </div>
                <div class="personal">
                    <PersonalStatus />
                </div>
            </div>
            {move || if let Some(hovered) = *hovered.read() {
                if Some(hovered) != *open.read() {
                    if !just_closed.get() {
                        match hovered {
                            SidebarOpen::Roster => view! {
                                <div class="sidebar-drawer sidebar-hovering-drawer">
                                    <RosterList />
                                </div>
                            }.into_any(),
                            SidebarOpen::Chats => view! {
                                <div class="sidebar-drawer sidebar-hovering-drawer">
                                    <ChatsList />
                                </div>
                            }.into_any(),
                        }
                    } else {
                        
                        view! {}.into_any()
                    }
                } else {
                    view! {}.into_any()
                }
            } else {
                    view! {}.into_any()
            }}
            {move || if let Some(opened) = *open.read() {
                match opened {
                    SidebarOpen::Roster => view! {
                        <div class="sidebar-drawer">
                            <RosterList />
                        </div>
                    }.into_any(),
                    SidebarOpen::Chats => view! {
                        <div class="sidebar-drawer">
                            <ChatsList />
                        </div>
                    }.into_any(),
                }
            } else {
                    view! {}.into_any()
            }}
        </div>
    }
}

#[component]
pub fn PersonalStatus() -> impl IntoView {
    let user: LocalResource<MacawUser> = use_context().expect("no local user in context");

    let (open, set_open) = signal(false);
    move || if let Some(user) = user.get() {
        let user: Store<User> = <ArcStore<filamento::user::User> as Clone>::clone(&(*user.user)).into();
        view! {
            <div class="dock-item" class:focused=move || *open.read()  on:click=move |_| {
                debug!("set open to true");
                set_open.update(|state| *state = !*state)
            }>
                <AvatarWithPresence user=user />
                <div class="dock-pill"></div>
            </div>
            {move || {
                let open = open.get();
                debug!("open = {:?}", open);
                if open {
                view! {
                    <Overlay set_open>
                        <PersonalStatusMenu user set_open/>
                    </Overlay>
                }.into_any()
            } else {
                view! {}.into_any()
            }}}
        }.into_any()
    } else {
        view! {}.into_any()
    }
}

#[component]
pub fn PersonalStatusMenu(user: Store<User>, set_open: WriteSignal<bool>) -> impl IntoView {
    let set_app: WriteSignal<AppState> = use_context().unwrap();
    let show_settings: RwSignal<Option<SettingsPage>> = use_context().unwrap();
    let user_presences: Store<UserPresences> = use_context().expect("no user presence store");
    
    let client = use_context::<Client>().expect("client not in context");
    let client1 = client.clone();
    let (show_value, set_show_value) = signal({
        let show = match user_presences.write().get_user_presences(&user.jid().read()).write().resource_presence(client.resource.read().clone().unwrap_or_default()).presence {
        PresenceType::Online(online) => match online.show {
            Some(s) => match s {
                Show::Away => 3,
                Show::Chat => 0,
                Show::DoNotDisturb => 2,
                Show::ExtendedAway => 4,
            },
            None => 1,
        },
        PresenceType::Offline(_offline) => 5,
    };
    debug!("initial show = {show}");
    show
    });

    let show_select: NodeRef<html::Select> = NodeRef::new();

    let disconnect = Action::new_local(move |()| {
        let client = client.clone();
        async move {
            client.disconnect(Offline::default()).await;
        }
    });
    let set_status = Action::new_local(move |show_value: &i32| {
        let show_value = show_value.to_owned();    
        let client = client1.clone();
        async move {
            if let Err(e) = match show_value {
                0 => {
                    if let Ok(r) = client.connect().await {
                        client.resource.set(Some(r))
                    };
                    client.set_status(Online { show: Some(Show::Chat), ..Default::default() }).await
                },
                1 => {
                    if let Ok(r) = client.connect().await {
                        client.resource.set(Some(r))
                    };
                    client.set_status(Online { show: None, ..Default::default() }).await
                },
                2 => {
                    if let Ok(r) = client.connect().await {
                        client.resource.set(Some(r))
                    };
                    client.set_status(Online { show: Some(Show::DoNotDisturb), ..Default::default() }).await
                },
                3 => {
                    if let Ok(r) = client.connect().await {
                        client.resource.set(Some(r))
                    };
                    client.set_status(Online { show: Some(Show::Away), ..Default::default() }).await
                },
                4 => {
                    if let Ok(r) = client.connect().await {
                        client.resource.set(Some(r))
                    };
                    client.set_status(Online { show: Some(Show::ExtendedAway), ..Default::default() }).await
                },
                5 => {
                    if let Ok(_) = client.disconnect(Offline::default()).await {
                        client.resource.set(None)
                    }
                    set_show_value.set(5);
                    return
                }
                _ => {
                    error!("invalid availability select");
                    return
                }
            } {
                error!("show set error: {e}");
                return
            }
            set_show_value.set(show_value);
        }
    });

    view! {
        <div class="personal-status-menu menu">
            <div class="user">
                <AvatarWithPresence user=user />
                <div class="user-info">
                    <div class="nick">{move || get_name(user, false)}</div>
                    <div class="jid">{move || user.jid().with(|jid| jid.to_string())}</div>
                </div>
            </div>
            <div class="status-edit">
                <select
                    node_ref=show_select
                    on:change:target=move |ev| {
                        let show_value = ev.target().value().parse().unwrap();
                        set_status.dispatch(show_value);
                    }
                    prop:show_value=move || show_value.get().to_string()
                >
                    <option value="0" selected=move || show_value.get_untracked() == 0>Available to Chat</option>
                    <option value="1" selected=move || show_value.get_untracked() == 1>Online</option>
                    <option value="2" selected=move || show_value.get_untracked() == 2>Do not disturb</option>
                    <option value="3" selected=move || show_value.get_untracked() == 3>Away</option>
                    <option value="4" selected=move || show_value.get_untracked() == 4>Extended Away</option>
                    <option value="5" selected=move || show_value.get_untracked() == 5>Offline</option>
                </select>
            </div>
            <hr />
            <div class="menu-item" on:click=move |_| {
                show_settings.set(Some(SettingsPage::Profile));
                set_open.set(false);
            }>
                Profile
            </div>
            <div class="menu-item" on:click=move |_| {
                show_settings.set(Some(SettingsPage::Account));
                set_open.set(false);
            }>
                Settings
            </div>
            <hr />
            <div class="menu-item" on:click=move |_| {
                // TODO: check if client is actually dropped/shutdown eventually
                disconnect.dispatch(());
                set_app.set(AppState::LoggedOut)
            }>
                Log out
            </div>
        </div>
    }
}

#[component]
pub fn Overlay(set_open: WriteSignal<bool>, children: Children) -> impl IntoView {
    view! {
        <div class="overlay">
            <div class="overlay-background" on:click=move |_| {
                debug!("set open to false");
                set_open.update(|state| *state = false)
            }></div>
            <div class="overlay-content">{children()}</div>
        </div>
    }
}

#[component]
pub fn Modal(on_background_click: impl Fn(MouseEvent) + 'static, children: Children) -> impl IntoView {
    view! {
        <div class="modal" on:click=move |e| {
            if e.current_target() == e.target() {
                on_background_click(e)
            }
        }>
            {children()}
        </div>
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SettingsPage {
    Account,
    Chat,
    Profile,
    Privacy,
}

#[component]
pub fn Settings() -> impl IntoView {
    let show_settings: RwSignal<Option<SettingsPage>> = use_context().unwrap();

    view! {
        <Modal on_background_click=move |_| { show_settings.set(None); }>
            <div class="settings panel">
                <div class="header">
                    <h2>Settings</h2>
                    <div class="header-icon close">
                    <IconComponent icon=Icon::Close24 on:click=move |_| show_settings.set(None)/>
                    </div>
                </div>
                <div class="settings-main">
                    <div class="settings-sidebar">
                        <div class:open=move || *show_settings.read() == Some(SettingsPage::Account) on:click=move |_| show_settings.set(Some(SettingsPage::Account))>Account</div>
                        <div class:open=move || *show_settings.read() == Some(SettingsPage::Chat) on:click=move |_| show_settings.set(Some(SettingsPage::Chat))>Chat</div>
                        <div class:open=move || *show_settings.read() == Some(SettingsPage::Privacy) on:click=move |_| show_settings.set(Some(SettingsPage::Privacy))>Privacy</div>
                        <div class:open=move || *show_settings.read() == Some(SettingsPage::Profile) on:click=move |_| show_settings.set(Some(SettingsPage::Profile))>Profile</div>
                    </div>
                    <div class="settings-page">
                        {move || if let Some(page) = show_settings.get() {
                            match page {
                            SettingsPage::Account => view! { <div>"account"</div> }.into_any(),
                            SettingsPage::Chat => view! { <div>"chat"</div> }.into_any(),
                            SettingsPage::Profile => view! { <ProfileSettings /> }.into_any(),
                            SettingsPage::Privacy => view! { <div>"privacy"</div> }.into_any(),
                            }
                        } else {
                            view! {}.into_any()
                        }}
                    </div>
                </div>
            </div>
        </Modal>
    }
}

#[derive(Debug, Clone, Error)]
pub enum ProfileSaveError {
    #[error("avatar publish: {0}")]
    Avatar(#[from] CommandError<AvatarPublishError<Files>>),
}

#[component]
pub fn ProfileSettings() -> impl IntoView {
    let client: Client = use_context().expect("no client in context");

    // TODO: compartmentalise into error component, form component...
    let (error, set_error) = signal(None::<ProfileSaveError>);
    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 (profile_save_pending, set_profile_save_pending) = signal(false);

    let profile_upload_data = RwSignal::new(None::<Vec<u8>>);
    let from_input = move |ev: Event| {
        let elem = ev.target().unwrap().unchecked_into::<HtmlInputElement>();
        
        // let UploadSignal(file_signal) = expect_context();
        // file_signal.update(Vec::clear); // Clear list from previous change
        let files = elem.files().unwrap_throw();

        if let Some(file) = files.get(0) {
            let reader = FileReader::new().unwrap_throw();
            // let name = file.name();

            // This closure only needs to be called a single time as we will just
            // remake it on each loop
            // * web_sys drops it for us when using this specific constructor
            let read_file = {
                // FileReader is cloned prior to moving into the closure
                let reader = reader.to_owned();
                Closure::once_into_js(move |_: ProgressEvent| {
                    // `.result` valid after the `read_*` completes on FileReader
                    // https://developer.mozilla.org/en-US/docs/Web/API/FileReader/result
                    let result = reader.result().unwrap_throw();
                    let data= Uint8Array::new(&result).to_vec();
                    // Do whatever you want with the Vec<u8>
                    profile_upload_data.set(Some(data));
                })
            };
            reader.set_onloadend(Some(read_file.as_ref().unchecked_ref()));

            // read_as_array_buffer takes a &Blob
            //
            // Per https://w3c.github.io/FileAPI/#file-section
            // > A File object is a Blob object with a name attribute..
            //
            // File is a subclass (inherits) from the Blob interface, so a File
            // can be used anywhere a Blob is required.
            reader.read_as_array_buffer(&file).unwrap_throw();

            // You can also use `.read_as_text(&file)` instead if you just want a string.
            // This example shows how to extract an array buffer as it is more flexible
            //
            // If you use `.read_as_text` change the closure from ...
            //
            // let result = reader.result().unwrap_throw();
            // let vec_of_u8_bytes = Uint8Array::new(&result).to_vec();
            // let content = String::from_utf8(vec_of_u8_bytes).unwrap_throw();
            //
            // to ...
            //
            // let result = reader.result().unwrap_throw();
            // let content = result.as_string().unwrap_throw();
        } else {
            profile_upload_data.set(None);
        }
    };

    let save_profile = Action::new_local(move |_| {
        let client = client.clone();
        async move {}
    });

    let new_nick= RwSignal::new("".to_string());

    view! {
        <div class="profile-settings">
            <form on:submit=move |ev| {
                    ev.prevent_default();
                    save_profile.dispatch(());
            }>
                {error_message}
                <div class="change-avatar">
                    <input type="file" id="client-user-avatar" on:change=from_input />
                </div>
                <input disabled=profile_save_pending placeholder="Nickname" type="text" id="client-user-nick" bind:value=new_nick name="client-user-nick" />
                <input disabled=profile_save_pending class="button" type="submit" value="Save Changes" />
            </form>
        </div>
        <div class="profile-preview">
            <h2>Profile Preview</h2>
            <div class="preview">
                <img />
                <div>nick</div>
            </div>
        </div>
    }
}

#[component]
pub fn OpenChatsPanelView() -> impl IntoView {
    let open_chats: Store<OpenChatsPanel> = use_context().expect("no open chats panel in context");

    // TODO: tabs
    // view! {
    //     {move || {
    //         if open_chats.chats().read().len() > 1 {
    //             Some(
    //                 view! {
    //                     <For
    //                         each=move || open_chats.chats().get()
    //                         key=|(jid, _)| jid.clone()
    //                         let(chat)
    //                     ></For>
    //                 },
    //             )
    //         } else {
    //             None
    //         }
    //     }}
    // }
    view! {
        <div class="open-chat-views">
            {move || {
                if let Some(open_chat) = open_chats.chat_view().get() {
                    if let Some(open_chat) = open_chats.chats().read().get(&open_chat) {
                        view! { <OpenChatView chat=open_chat.clone() /> }.into_any()
                    } else {
                        view! {}.into_any()
                    }
                } else {
                    view! {}.into_any()
                }
            }}
        </div>
    }
}

#[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_jid = move || chat_chat.correspondent().get();

    view! {
        <div class="open-chat-view">
            <ChatViewHeader chat=chat.clone() />
            <MessageHistoryBuffer chat=chat.clone() />
            <ChatViewMessageComposer chat=chat_jid() />
        </div>
    }
}

pub fn show_to_icon(show: Show) -> Icon {
    match show {
        Show::Away => Icon::Away16Color,
        Show::Chat => Icon::Chat16Color,
        Show::DoNotDisturb => Icon::Dnd16Color,
        Show::ExtendedAway => Icon::Xa16Color,
    }
}

#[component]
pub fn AvatarWithPresence(user: Store<User>) -> impl IntoView {
    let avatar = LocalResource::new(move || get_avatar(user));
    let user_presences: Store<UserPresences> = use_context().expect("no user presences in context");
    let presence = move || user_presences.write().get_user_presences(&user.read().jid).read().presence();
    let show_icon = move || presence().map(|(_, presence)| {
        match presence.presence {
            PresenceType::Online(online) => if let Some(show) = online.show {
                Some(show_to_icon(show))
            } else {
                Some(Icon::Available16Color)
            },
            PresenceType::Offline(offline) => None,
        }
    }).unwrap_or_default();

    view! {
        <div class="avatar-with-presence">
        <img class="avatar" src=move || avatar.get() />
        {move || if let Some(icon) = show_icon() {
            view!{
                <IconComponent icon=icon class:presence-show-icon=true />
            }.into_any()
        } else {
            view! {}.into_any()
        }}
        </div>
    }
}

#[component]
pub fn ChatViewHeader(chat: MacawChat) -> impl IntoView {
    let chat_user = <ArcStore<filamento::user::User> as Clone>::clone(&chat.user).into();
    let name = move || get_name(chat_user, true);
    let jid = move || chat_user.jid().read().to_string();

    view! {
        <div class="chat-view-header panel">
            <AvatarWithPresence user=chat_user />
            <div class="user-info">
                <h2 class="name">{name}</h2>
                <h3>{jid}</h3>
            </div>
        </div>
    }
}

#[component]
pub fn MessageHistoryBuffer(chat: MacawChat) -> impl IntoView {
    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())
                .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_set = set_messages.clone();
    let _load_new_messages = LocalResource::new(move || {
        let load_new_messages_set = load_new_messages_set.clone();
        async move {
            load_messages.await;
            let (sub_id, mut new_messages) = new_messages_signal
                .write()
                .subscribe_chat(chat_chat.correspondent().get());
            set_sub_id.set(Some(sub_id));
            while let Some(new_message) = new_messages.recv().await {
                debug!("got new message in let message buffer");
                let mut messages = load_new_messages_set.write();
                if let Some((_, last)) = messages.last() {
                    if *<ArcStore<filamento::chat::Message> as Clone>::clone(&last.message)
                        .timestamp()
                        .read()
                        < *<ArcStore<filamento::chat::Message> as Clone>::clone(&new_message)
                            .timestamp()
                            .read()
                    {
                        messages.insert(
                            <ArcStore<filamento::chat::Message> as Clone>::clone(
                                &new_message.message,
                            )
                            .id()
                            .get(),
                            new_message,
                        );
                        debug!("set the new message in message buffer");
                    } else {
                        let index = match messages.binary_search_by(|_, value| {
                            <ArcStore<filamento::chat::Message> as Clone>::clone(&value.message)
                                .timestamp()
                                .read()
                                .cmp(
                                    &<ArcStore<filamento::chat::Message> as Clone>::clone(
                                        &new_message.message,
                                    )
                                    .timestamp()
                                    .read(),
                                )
                        }) {
                            Ok(i) => i,
                            Err(i) => i,
                        };
                        messages.insert_before(
                            // TODO: check if this logic is correct
                            index,
                            <ArcStore<filamento::chat::Message> as Clone>::clone(
                                &new_message.message,
                            )
                            .id()
                            .get(),
                            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(),
                        new_message,
                    );
                    debug!("set the new message in message buffer");
                }
            }
        }
    });
    on_cleanup(move || {
        if let Some(sub_id) = sub_id.get() {
            new_messages_signal
                .write()
                .unsubscribe_chat(sub_id, chat_chat.correspondent().get());
        }
    });

    let each = move || {
        let mut last_timestamp = NaiveDateTime::MIN;
        let mut last_user: Option<BareJID> = 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();
                // TODO: mark new day
                // if message_timestamp.date() > last_timestamp.date() {
                //     messages_view = messages_view.push(date(message_timestamp.date()));
                // }
                let major = if last_user.as_ref() != Some(&message.message.read().from)
                    || message_timestamp - last_timestamp > TimeDelta::minutes(3)
                {
                    true
                } else {
                    false
                };
                last_user = Some(
                    <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
        }
        messages.into_iter().rev()
    };

    view! {
        <div class="messages-buffer">
            <For each=each key=|message| (message.0, message.1.1, message.1.2) let(message)>
                <Message message=message.1.0 major=message.1.1 r#final=message.1.2 />
            </For>
        </div>
    }
}

#[derive(Copy, Clone)]
pub enum Icon {
    AddContact24,
    Attachment24,
    Away16,
    Away16Color,
    Bubble16,
    Bubble16Color,
    Bubble24,
    Close24,
    Contact24,
    Delivered16,
    Dnd16,
    Dnd16Color,
    Error16Color,
    Forward24,
    Heart24,
    NewBubble24,
    Reply24,
    Sending16,
    Sent16,
    Chat16Color,
    Xa16Color,
    Available16Color,
}

pub const ICONS_SRC: &str = "/assets/icons/";

impl Icon {
    pub fn src(&self) -> String {
        match self {
            Icon::AddContact24 => format!("{}addcontact24.svg", ICONS_SRC),
            Icon::Attachment24 => format!("{}attachment24.svg", ICONS_SRC),
            Icon::Away16 => format!("{}away16.svg", ICONS_SRC),
            Icon::Away16Color => format!("{}away16color.svg", ICONS_SRC),
            Icon::Bubble16 => format!("{}bubble16.svg", ICONS_SRC),
            Icon::Bubble16Color => format!("{}bubble16color.svg", ICONS_SRC),
            Icon::Bubble24 => format!("{}bubble24.svg", ICONS_SRC),
            Icon::Close24 => format!("{}close24.svg", ICONS_SRC),
            Icon::Contact24 => format!("{}contact24.svg", ICONS_SRC),
            Icon::Delivered16 => format!("{}delivered16.svg", ICONS_SRC),
            Icon::Dnd16 => format!("{}dnd16.svg", ICONS_SRC),
            Icon::Dnd16Color => format!("{}dnd16color.svg", ICONS_SRC),
            Icon::Error16Color => format!("{}error16color.svg", ICONS_SRC),
            Icon::Forward24 => format!("{}forward24.svg", ICONS_SRC),
            Icon::Heart24 => format!("{}heart24.svg", ICONS_SRC),
            Icon::NewBubble24 => format!("{}newbubble24.svg", ICONS_SRC),
            Icon::Reply24 => format!("{}reply24.svg", ICONS_SRC),
            Icon::Sending16 => format!("{}sending16.svg", ICONS_SRC),
            Icon::Sent16 => format!("{}sent16.svg", ICONS_SRC),
            Icon::Chat16Color => format!("{}chat16color.svg", ICONS_SRC),
            Icon::Xa16Color => format!("{}xa16color.svg", ICONS_SRC),
            Icon::Available16Color => format!("{}available16color.svg", ICONS_SRC),
        }
    }

    pub fn size(&self) -> isize {
        match self {
            Icon::AddContact24 => 24,
            Icon::Attachment24 => 24,
            Icon::Away16 => 16,
            Icon::Away16Color => 16,
            Icon::Bubble16 => 16,
            Icon::Bubble16Color => 16,
            Icon::Bubble24 => 24,
            Icon::Close24 => 24,
            Icon::Contact24 => 24,
            Icon::Delivered16 => 16,
            Icon::Dnd16 => 16,
            Icon::Dnd16Color => 16,
            Icon::Error16Color => 16,
            Icon::Forward24 => 24,
            Icon::Heart24 => 24,
            Icon::NewBubble24 => 24,
            Icon::Reply24 => 24,
            Icon::Sending16 => 16,
            Icon::Sent16 => 16,
            Icon::Chat16Color => 16,
            Icon::Xa16Color => 16,
            Icon::Available16Color => 16,
        }
    }

    pub fn light(&self) -> bool {
        match self {
            Icon::AddContact24 => true,
            Icon::Attachment24 => true,
            Icon::Away16 => true,
            Icon::Away16Color => false,
            Icon::Bubble16 => true,
            Icon::Bubble16Color => false,
            Icon::Bubble24 => true,
            Icon::Close24 => true,
            Icon::Contact24 => true,
            Icon::Delivered16 => true,
            Icon::Dnd16 => true,
            Icon::Dnd16Color => false,
            Icon::Error16Color => false,
            Icon::Forward24 => true,
            Icon::Heart24 => true,
            Icon::NewBubble24 => true,
            Icon::Reply24 => true,
            Icon::Sending16 => true,
            Icon::Sent16 => true,
            Icon::Chat16Color => false,
            Icon::Xa16Color => false,
            Icon::Available16Color => false,
        }
    }
}

#[component]
pub fn IconComponent(icon: Icon) -> impl IntoView {
    view! {
        <img class:light=icon.light() class:icon=true style=move || format!("height: {}px; width: {}px", icon.size(), icon.size()) src=move || icon.src() />
    }
}

#[component]
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()
        }
        // 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()
        }
        // 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()
        }
        // TODO: queued icon
        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_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, false);

    // TODO: chrono-humanize?
    // TODO: if final, show delivery not only on hover.
    // {move || message_message.delivery().read().map(|delivery| delivery.to_string()).unwrap_or_default()}
    if major {
        view! {
            <div class:final=r#final class="chat-message major">
                    <div class="left">
                    <Transition fallback=|| view! { <img class="avatar" src=NO_AVATAR /> } >
                        <img class="avatar" src=move || avatar.get() />
                    </Transition>
                    </div>
                <div class="middle">
                    <div class="message-info">
                        <div class="message-user-name">{name}</div>
                        <div class="message-timestamp">{move || message_message.timestamp().read().format("%H:%M").to_string()}</div>
                    </div>
                    <div class="message-text">
                        {move || message_message.body().read().body.clone()}
                    </div>
                </div>
                <div class="right message-delivery">{move || message_message.delivery().get().map(|delivery| view! { <Delivery class:light=true delivery /> } ) }</div>
            </div>
        }.into_any()
    } else {
        view! {
            <div class:final=r#final class="chat-message minor">
                <div class="left message-timestamp">
                    {move || message_message.timestamp().read().format("%H:%M").to_string()}
                </div>
                <div class="middle message-text">{move || message_message.body().read().body.clone()}</div>
                <div class="right message-delivery">{move || message_message.delivery().get().map(|delivery| view! { <Delivery delivery /> } ) }</div>
            </div>
        }.into_any()
    }
}

#[component]
pub fn ChatViewMessageComposer(chat: BareJID) -> impl IntoView {
    let message_input: NodeRef<Div> = NodeRef::new();

    // TODO: load last message draft
    let new_message = RwSignal::new("".to_string());
    let client: Client = use_context().expect("no client in context");
    let client = RwSignal::new(client);
    let (shift_pressed, set_shift_pressed) = signal(false);

    let send_message = move || {
        let value = chat.clone();
        spawn_local(async move {
            match client
                .read()
                .send_message(
                    value,
                    Body {
                        body: new_message.get(),
                    },
                )
                .await
            {
                Ok(_) => {
                    new_message.set("".to_string());
                    message_input
                        .write()
                        .as_ref()
                        .expect("message input div not mounted")
                        .set_text_content(Some(""));
                }
                Err(e) => tracing::error!("message send error: {}", e),
            }
        })
    };

    let _focus = Effect::new(move |_| {
        if let Some(input) = message_input.get() {
            let _ = input.focus();
            // TODO: set the last draft
            input.set_text_content(Some(""));
            // input.style("height: 0");
            // let height = input.scroll_height();
            // input.style(format!("height: {}px", height));
        }
    });

    // let on_input = move |ev: Event| {
    //     // let keyboard_event: KeyboardEvent = ev.try_into().unwrap();
    //     debug!("got input event");
    //     let key= event_target_value(&ev);
    //     new_message.set(key);
    //     debug!("set new message");
    // };
    //

    // TODO: placeholder
    view! {
        <form
            class="new-message-composer panel"
        >
            <div
                class="text-box"
                on:input:target=move |ev| new_message.set(ev.target().text_content().unwrap_or_default())
                node_ref=message_input
                contenteditable
                on:keydown=move |ev| {
                    match ev.key_code() {
                        16 => set_shift_pressed.set(true),
                        13 => if !shift_pressed.get() {
                            ev.prevent_default();
                            send_message();
                        }
                        _ => {}
                        // debug!("shift pressed down");
                    }
                }
                on:keyup=move |ev| {
                    match ev.key_code()  {
                        16 => set_shift_pressed.set(false),
                        _ => {}
                        // debug!("shift released");
                    }
                }
            ></div>
            // <input hidden type="submit" />
        </form>
    }
}

// V has to be an arc signal
#[derive(Debug)]
struct ArcStateStore<K, V> {
    store: Arc<RwLock<HashMap<K, (V, usize)>>>,
}

impl<K, V> PartialEq for ArcStateStore<K, V> {
    fn eq(&self, other: &Self) -> bool {
        Arc::ptr_eq(&self.store, &other.store)
    }
}

impl<K, V> Clone for ArcStateStore<K, V> {
    fn clone(&self) -> Self {
        Self {
            store: Arc::clone(&self.store),
        }
    }
}

impl<K, V> Eq for ArcStateStore<K, V> {}

impl<K, V> ArcStateStore<K, V> {
    pub fn new() -> Self {
        Self {
            store: Arc::new(RwLock::new(HashMap::new())),
        }
    }
}

#[derive(Debug)]
struct StateStore<K, V, S = SyncStorage> {
    inner: ArenaItem<ArcStateStore<K, V>, S>,
}

impl<K, V, S> Dispose for StateStore<K, V, S> {
    fn dispose(self) {
        self.inner.dispose()
    }
}

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>>,
{
    pub fn new_with_storage() -> Self {
        Self {
            inner: ArenaItem::new_with_storage(ArcStateStore::new()),
        }
    }
}

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>
{
    fn from(value: ArcStateStore<K, V>) -> Self {
        Self {
            inner: ArenaItem::new_with_storage(value),
        }
    }
}

impl<K: 'static, V: 'static> FromLocal<ArcStateStore<K, V>> for StateStore<K, V, LocalStorage> {
    fn from_local(value: ArcStateStore<K, V>) -> Self {
        Self {
            inner: ArenaItem::new_with_storage(value),
        }
    }
}

impl<K, V, S> Copy for StateStore<K, V, S> {}

impl<K, V, S> Clone for StateStore<K, V, S> {
    fn clone(&self) -> Self {
        *self
    }
}

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();
            let mut store = store.store.write().unwrap();
            if let Some((v, count)) = store.get_mut(&key) {
                *v = value.clone();
                *count += 1;
            } else {
                store.insert(key.clone(), (value.clone(), 1));
            }
        };
        StateListener {
            value,
            cleaner: StateCleaner {
                key,
                state_store: self.clone(),
            },
        }
    }
}

impl<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();
        if let Some((v, _)) = store.get_mut(key) {
            *v = value;
        }
    }

    pub fn modify(&self, key: &K, modify: impl Fn(&mut V)) {
        let store = self.inner.try_get_value().unwrap();
        let mut store = store.store.write().unwrap();
        if let Some((v, _)) = store.get_mut(key) {
            modify(v);
        }
    }

    fn remove(&self, key: &K) {
        // let store = self.inner.try_get_value().unwrap();
        // let mut store = store.store.write().unwrap();
        // if let Some((_v, count)) = store.get_mut(key) {
        //     *count -= 1;
        //     if *count == 0 {
        //         store.remove(key);
        //         debug!("dropped item from store");
        //     }
        // }
    }
}

#[derive(Clone)]
struct StateListener<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>,
}

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 {
        &self.value
    }
}

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

struct ArcStateCleaner<K, V> {
    key: K,
    state_store: ArcStateStore<K, V>,
}

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,
{
    fn clone(&self) -> Self {
        {
            let store = self.state_store.inner.try_get_value().unwrap();
            let mut store = store.store.write().unwrap();
            if let Some((_v, count)) = store.get_mut(&self.key) {
                *count += 1;
            }
        }
        Self {
            key: self.key.clone(),
            state_store: self.state_store.clone(),
        }
    }
}

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

#[derive(Clone)]
struct MacawChat {
    chat: StateListener<BareJID, ArcStore<Chat>>,
    user: StateListener<BareJID, ArcStore<User>>,
}

impl MacawChat {
    fn got_chat_and_user(chat: Chat, user: User) -> Self {
        let chat_state_store: StateStore<BareJID, ArcStore<Chat>> =
            use_context().expect("no chat state store");
        let user_state_store: StateStore<BareJID, 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 }
    }
}

impl Deref for MacawChat {
    type Target = StateListener<BareJID, ArcStore<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, ArcStore<Message>>,
    user: StateListener<BareJID, ArcStore<User>>,
}

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

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

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

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

#[derive(Clone)]
struct MacawUser {
    user: StateListener<BareJID, ArcStore<User>>,
}

impl MacawUser {
    fn got_user(user: User) -> Self {
        
        let user_state_store: StateStore<BareJID, ArcStore<User>> =
            use_context().expect("no user state store");
        let user = user_state_store.store(user.jid.clone(), ArcStore::new(user));
        Self { user }
    }
}

impl Deref for MacawUser {
    type Target = StateListener<BareJID, ArcStore<User>>;

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

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

#[derive(Clone)]
struct MacawContact {
    contact: Store<Contact>,
    user: StateListener<BareJID, ArcStore<User>>,
}

impl MacawContact {
    fn got_contact_and_user(contact: Contact, user: User) -> Self {
        let contact = Store::new(contact);
        let user_state_store: StateStore<BareJID, ArcStore<User>> =
            use_context().expect("no user state store");
        let user = user_state_store.store(user.jid.clone(), ArcStore::new(user));
        Self { contact, user }
    }
}

impl Deref for MacawContact {
    type Target = Store<Contact>;

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

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

#[component]
fn ChatsList() -> impl IntoView {
    let (chats, set_chats) = signal(IndexMap::new());

    let load_chats = LocalResource::new(move || async move {
        let client = use_context::<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<BareJID, _>>();
                set_chats.set(chats);
            }
            Err(_) => {
                // TODO: show error message at top of chats list
            }
        }
    });

    let (open_new_chat, set_open_new_chat) = signal(false);

    // TODO: filter new messages signal
    let new_messages_signal: RwSignal<MessageSubscriptions> = use_context().unwrap();
    let (sub_id, set_sub_id) = signal(None);
    let _load_new_messages = LocalResource::new(move || async move {
        load_chats.await;
        let (sub_id, mut new_messages) = new_messages_signal.write().subscribe_all();
        set_sub_id.set(Some(sub_id));
        while let Some((to, new_message)) = new_messages.recv().await {
            debug!("got new message in let");
            let mut chats = set_chats.write();
            if let Some((chat, _latest_message)) = chats.shift_remove(&to) {
                // TODO: check if new message is actually latest message
                debug!("chat existed");
                debug!("new message: {}", new_message.read().body.body);
                chats.insert_before(0, to, (chat.clone(), new_message));
                debug!("done setting");
            } else {
                debug!("the chat didn't exist");
                let client = use_context::<Client>().expect("client not in context");
                let chat = client.get_chat(to.clone()).await.unwrap();
                let user = client.get_user(to.clone()).await.unwrap();
                debug!("before got chat");
                let chat = MacawChat::got_chat_and_user(chat, user);
                debug!("after got chat");
                chats.insert_before(0, to, (chat, new_message));
                debug!("done setting");
            }
        }
        debug!("set the new message");
    });
    on_cleanup(move || {
        if let Some(sub_id) = sub_id.get() {
            new_messages_signal.write().unsubscribe_all(sub_id);
        }
    });

    view! {
        <div class="chats-list panel">
            // TODO: update icon, tooltip on hover.
            <div class="header">
                <h2>Chats</h2>
                <div class="new-chat header-icon" class:open=open_new_chat >
                    <IconComponent icon=Icon::NewBubble24 on:click=move |_| set_open_new_chat.update(|state| *state = !*state)/>
                    {move || {
                        if *open_new_chat.read() {
                            view! {
                                <Overlay set_open=set_open_new_chat>
                                    <NewChatWidget set_open_new_chat />
                                </Overlay>
                            }.into_any()
                        } else {
                            view! {}.into_any()
                        }
                    }}
                </div>
            </div>
            <div class="chats-list-chats">
                <For each=move || chats.get() key=|chat| chat.1.1.message.read().id let(chat)>
                    <ChatsListItem chat=chat.1.0 message=chat.1.1 />
                </For>
            </div>
        </div>
    }
}

#[derive(Clone, Debug, Error)]
pub enum NewChatError {
    #[error("Missing JID")]
    MissingJID,
    #[error("Invalid JID: {0}")]
    InvalidJID(#[from] jid::ParseError),
    #[error("Database: {0}")]
    Db(#[from] CommandError<DatabaseError>),
}

#[component]
fn NewChatWidget(set_open_new_chat: WriteSignal<bool>) -> impl IntoView {
    let jid = RwSignal::new("".to_string());

    // TODO: compartmentalise into error component, form component...
    let (error, set_error) = signal(None::<NewChatError>);
    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 (new_chat_pending, set_new_chat_pending) = signal(false);
 
    let open_chats: Store<OpenChatsPanel> =
        use_context().expect("no open chats panel store in context");
    let client = use_context::<Client>().expect("client not in context");

    let chat_state_store: StateStore<BareJID, ArcStore<Chat>> =
        use_context().expect("no chat state store");
    let user_state_store: StateStore<BareJID, ArcStore<User>> =
        use_context().expect("no user state store");

    let open_chat = Action::new_local(move |_| {
        let client = client.clone();
        async move {
            set_new_chat_pending.set(true);

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

            let jid = match JID::from_str(&jid.read_untracked()) {
                // TODO: ability to direct address a resource?
                Ok(j) => j.to_bare(),
                Err(e) => {
                    set_error.set(Some(e.into()));
                    set_new_chat_pending.set(false);
                    return;
                }
            };

            let chat_jid = jid;
            let (chat, user) = match client.get_chat_and_user(chat_jid).await {
                Ok(c) => c,
                Err(e) => {
                    set_error.set(Some(e.into()));
                    set_new_chat_pending.set(false);
                    return;
                },
            };

            let chat = {
                let user = user_state_store.store(user.jid.clone(), ArcStore::new(user));
                let chat = chat_state_store.store(chat.correspondent.clone(), ArcStore::new(chat));
                MacawChat { chat, user }
            };
            open_chats.update(|open_chats| open_chats.open(chat.clone()));
            set_open_new_chat.set(false);
        }
    });

    let jid_input = NodeRef::<Input>::new();
    let _focus = Effect::new(move |_| {
        if let Some(input) = jid_input.get() {
            let _ = input.focus();
            input.set_text_content(Some(""));
            // input.style("height: 0");
            // let height = input.scroll_height();
            // input.style(format!("height: {}px", height));
        }
    });

    view! {
        <div class="new-chat-widget">
            <form on:submit=move |ev| {
                ev.prevent_default();
                open_chat.dispatch(());
            }>
                {error_message}
                <input
                    disabled=new_chat_pending
                    placeholder="JID"
                    type="text"
                    node_ref=jid_input
                    bind:value=jid
                    name="jid"
                    id="jid"
                    autofocus="true"
                />
                <input disabled=new_chat_pending class="button" type="submit" value="Start Chat" />
            </form>
        </div>
    }
}

#[component]
fn RosterList() -> impl IntoView {
    let requests: ReadSignal<HashSet<BareJID>> = use_context().expect("no pending subscriptions in context");

    let roster: Store<Roster> = use_context().expect("no roster in context");
    let (open_add_contact, set_open_add_contact) = signal(false);

    // TODO: filter new messages signal
    view! {
        <div class="roster-list panel">
            <div class="header">
                <h2>Roster</h2>
                <div class="add-contact header-icon" class:open=open_add_contact>
                    <IconComponent icon=Icon::AddContact24 on:click=move |_| set_open_add_contact.update(|state| *state = !*state)/>
                    {move || {
                        if !requests.read().is_empty() {
                            view! {
                                <div class="badge"></div>
                            }.into_any()
                        } else {
                            view! {}.into_any()
                        }
                    }}
                </div>
            </div>
            {move || {
                if *open_add_contact.read() {
                    view! {
                        <div class="roster-add-contact">
                            <AddContact />
                        </div>
                    }.into_any()
                } else {
                    view! {}.into_any()
                }
            }}
            <div class="roster-list-roster">
                <For each=move || roster.contacts().get() key=|contact| contact.0.clone() let(contact)>
                    <RosterListItem contact=contact.1 />
                </For>
            </div>
        </div>
    }
}

#[derive(Clone, Debug, Error)]
pub enum AddContactError {
    #[error("Missing JID")]
    MissingJID,
    #[error("Invalid JID: {0}")]
    InvalidJID(#[from] jid::ParseError),
    #[error("Subscription: {0}")]
    Db(#[from] CommandError<SubscribeError>),
}

#[component]
fn AddContact() -> impl IntoView {
    let requests: ReadSignal<HashSet<BareJID>> = use_context().expect("no pending subscriptions in context");
    let set_requests: WriteSignal<HashSet<BareJID>> = use_context().expect("no pending subscriptions write signal in context");
    let roster: Store<Roster>  = use_context().expect("no roster in context");

    let jid = RwSignal::new("".to_string());
    // TODO: compartmentalise into error component, form component...
    let (error, set_error) = signal(None::<AddContactError>);
    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 (add_contact_pending, set_add_contact_pending) = signal(false);

    let client = use_context::<Client>().expect("client not in context");
    let client2 = client.clone();
    let client3 = client.clone();
    let client4 = client.clone();

    let add_contact= Action::new_local(move |_| {
        let client = client.clone();
        async move {
            set_add_contact_pending.set(true);

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

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

            let chat_jid = jid;
            // TODO: more options?
            match client.buddy_request(chat_jid).await {
                Ok(c) => c,
                Err(e) => {
                    set_error.set(Some(e.into()));
                    set_add_contact_pending.set(false);
                   return;
                },
            };

            set_add_contact_pending.set(false);
        }
    });

    let jid_input = NodeRef::<Input>::new();
    let _focus = Effect::new(move |_| {
        if let Some(input) = jid_input.get() {
            let _ = input.focus();
            input.set_text_content(Some(""));
            // input.style("height: 0");
            // let height = input.scroll_height();
            // input.style(format!("height: {}px", height));
        }
    });

    let outgoing = move || roster.contacts().get().into_iter().filter(|(jid, contact)| {
        match *contact.contact.subscription().read() {
            filamento::roster::Subscription::None => false,
            filamento::roster::Subscription::PendingOut => true,
            filamento::roster::Subscription::PendingIn => false,
            filamento::roster::Subscription::PendingInPendingOut => true,
            filamento::roster::Subscription::OnlyOut => false,
            filamento::roster::Subscription::OnlyIn => false,
            filamento::roster::Subscription::OutPendingIn => false,
            filamento::roster::Subscription::InPendingOut => true,
            filamento::roster::Subscription::Buddy => false,
        }
    }).collect::<Vec<_>>();

    let accept_friend_request = Action::new_local(move |jid: &BareJID| {
        let client = client2.clone();
        let jid = jid.clone();
        async move {
            // TODO: error
            client.accept_buddy_request(jid).await;
        }
    });

    let reject_friend_request = Action::new_local(move |jid: &BareJID| {
        let client = client3.clone();
        let jid = jid.clone();
        async move {
            // TODO: error
            client.unsubscribe_contact(jid.clone()).await;
            set_requests.write().remove(&jid);
        }
    });

    let cancel_subscription_request = Action::new_local(move |jid: &BareJID| {
        let client = client4.clone();
        let jid = jid.clone();
        async move {
            // TODO: error
            client.unsubscribe_from_contact(jid).await;

        }
    });

    view! {
        <div class="add-contact-menu">
        <div>
            {error_message}
            <form on:submit=move |ev| {
                ev.prevent_default();
                add_contact.dispatch(());
            }>
                <input
                    disabled=add_contact_pending
                    placeholder="JID"
                    type="text"
                    node_ref=jid_input
                    bind:value=jid
                    name="jid"
                    id="jid"
                    autofocus="true"
                />
                <input disabled=add_contact_pending class="button" type="submit" value="Send Friend Request" />
            </form>
        </div>
        {move || if !requests.read().is_empty() {
            view! {
                <div>
                    <h3>Incoming Subscription Requests</h3>
                    <For each=move || requests.get() key=|request| request.clone() let(request)>
                        {
                            let request2 = request.clone();
                            let request3 = request.clone();
                            let jid_string = move || request.to_string();
                            view! {
                            <div class="jid-with-button"><div class="jid">{jid_string}</div>
                            <div><div class="button" on:click=move |_| { accept_friend_request.dispatch(request2.clone()); } >Accept</div><div class="button" on:click=move |_| { reject_friend_request.dispatch(request3.clone()); } >Reject</div></div></div>
                            }
                        }
                    </For>
                </div>
            }.into_any()
        } else {
            view! {}.into_any()
        }}
        {move || if !outgoing().is_empty() {
            view! {
                <div>
                    <h3>Pending Outgoing Subscription Requests</h3>
                    <For each=move || outgoing() key=|(jid, _contact)| jid.clone() let((jid, contact))>
                        {
                            let jid2 = jid.clone();
                            let jid_string = move || jid.to_string();
                            view! {
                            <div class="jid-with-button"><div class="jid">{jid_string}</div><div class="button" on:click=move |_| { cancel_subscription_request.dispatch(jid2.clone()); } >Cancel</div></div>
                            }
                        }
                    </For> 
                </div>
            }.into_any()
        } else {
            view! {}.into_any()
        }}
        </div>
    }
}

#[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 name = move || get_name(contact_user, false);

    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 = MacawChat::got_chat_and_user(chat, contact_user.get());

    let open_chat = move |_| {
        debug!("opening chat");
        open_chats.update(|open_chats| open_chats.open(chat.clone()));
    };

    let open = move || {
        if let Some(open_chat) = &*open_chats.chat_view().read() {
            debug!("got open chat: {:?}", open_chat);
            if *open_chat == *contact_user.jid().read() {
                return Open::Focused;
            }
        }
        if let Some(_backgrounded_chat) = open_chats
            .chats()
            .read()
            .get(contact_user.jid().read().deref())
        {
            return Open::Open;
        }
        Open::Closed
    };
    let focused = move || open().is_focused();
    let open = move || open().is_open();

    view! {
        <div class="roster-list-item" class:open=move || open() class:focused=move || focused() on:click=open_chat>
            <AvatarWithPresence user=contact_user />
            <div class="item-info">
                <div class="main-info"><p class="name">{name}<span class="jid"> - {move || contact_contact.user_jid().read().to_string()}</span></p></div>
                <div class="sub-info">{move || contact_contact.subscription().read().to_string()}</div>
            </div>
        </div>
    }
}

pub async fn get_avatar(user: Store<User>) -> String {
    if let Some(avatar) = &user.read().avatar {
        let client = use_context::<Client>().expect("client not in context");
        if let Some(data) = client.file_store.get_src(avatar).await {
            data
        } else {
            NO_AVATAR.to_string()
        }
        // TODO: enable avatar fetching
        // format!("/files/{}", avatar)
    } else {
        NO_AVATAR.to_string()
    }
}

pub fn get_name(user: Store<User>, note_to_self: bool) -> String {
    let roster: Store<Roster> = use_context().expect("no roster in context");
    if note_to_self {
        let client: Client = use_context().expect("no client in context");
        if *client.jid == *user.jid().read() {
            return "Note to self".to_string()
        }
    }
    if let Some(name) = roster
        .contacts()
        .read()
        .get(&user.read().jid)
        .map(|contact| contact.read().name.clone())
        .unwrap_or_default()
    {
        name.to_string()
    } else if let Some(nick) = &user.read().nick {
        nick.to_string()
    } else {
        user.read().jid.to_string()
    }
}

pub enum Open {
    /// Currently on screen
    Focused,
    /// Open in background somewhere (e.g. in another chat tab)
    Open,
    /// Closed
    Closed,
}

impl Open {
    pub fn is_focused(&self) -> bool {
        match self {
            Open::Focused => true,
            Open::Open => false,
            Open::Closed => false,
        }
    }

    pub fn is_open(&self) -> bool {
        match self {
            Open::Focused => true,
            Open::Open => true,
            Open::Closed => false,
        }
    }
}

#[component]
fn ChatsListItem(chat: MacawChat, message: MacawMessage) -> impl IntoView {
    let chat_chat: Store<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 message_message: Store<Message> = <ArcStore<Message> as Clone>::clone(&message.message).into();
    let name = move || get_name(chat_user, true);

    // TODO: store fine-grained reactivity
    let latest_message_body = move || message_message.body().get().body;
    let open_chats: Store<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 || {
        if let Some(open_chat) = &*open_chats.chat_view().read() {
            debug!("got open chat: {:?}", open_chat);
            if *open_chat == *chat_chat.correspondent().read() {
                return Open::Focused;
            }
        }
        if let Some(_backgrounded_chat) = open_chats
            .chats()
            .read()
            .get(chat_chat.correspondent().read().deref())
        {
            return Open::Open;
        }
        Open::Closed
    };
    let focused = move || open().is_focused();
    let open = move || open().is_open();

    let date = move || message_message.timestamp().read().naive_local();
    let now = move || Local::now().naive_local();
    let timeinfo = move || if date().date() == now().date() {
        // TODO: localisation/config
        date().time().format("%H:%M").to_string()
    } else {
        date().date().format("%d/%m").to_string()
    };

    view! {
        <div class="chats-list-item" class:open=move || open() class:focused=move || focused() on:click=open_chat>
            <AvatarWithPresence user=chat_user />
            <div class="item-info">
                <div class="main-info"><p class="name">{name}</p><p class="timestamp">{timeinfo}</p></div>
                <div class="sub-info"><p class="message-preview">{latest_message_body}</p><p><!-- "TODO: delivery or unread state" --></p></div>
            </div>
        </div>
    }
}