aboutsummaryrefslogblamecommitdiffstats
path: root/src/main.rs
blob: 8b50befd00e24a90dba07a9d1146d65475bbd729 (plain) (tree)
1
2
3
4
5
6
7
8
9

                               
                                         

                                

                               
                      
                            
 
                         
                                                    

                                                    

                                                            
                                       
                                  
                                                
                                                                   
                                                                                            
                                   
                                 
                                                           
                   
                                                                                                  
                                                                             
  
                                       
                                                                           
                
                                   
             
                   
                                     
                              
                                    
                     
                              
                                 
                                           
                                  

               
          
                
                 
 
                                        

                       

                                
                                              



                          



                               
                                                                 
         

     
 


                                                                                                                                                                                                                                                                 
                  
                    
                   

                                              
                              





                                            

 



                         

 

                              





                                      
                                




                                                  
                       



                                                                                   

 

                          





                                      
                             




                                                  
                       

                         
               















                                                  
                       

                      
                    

 








                                         





                                      
                                




                                                  




                                      
     
 
 


                                                  


     

                   
            
                                                                



                                                
                                                                



                            
                   
                                      
                                                  
                           


                                        





                     
                          

 



















                                                                                

                       

                                     
             







                                            



                                       
















                                                 








                                                  
                                           



                                      

 



                   



                                                              
                  



                                                                                 
                                                                       

                      







































                                                                                                
            














































                                                                                                
     
             

 

                                 

                                    
                                                                     


































                                                                                                  





                                      



                                           

                                                                                         



                                                                                     
 
                                                                      

                                                                 
                    

                                                 
                                 


































                                                                                       
                    
                                          
                             




                                                                                 







                                                             
                             
                                                         

                          
                                  




                                                                                   





                                                            

                                         

             
                                              
                                    
                                             
                                                              


















                                                                             
                                       
                                




                                               
                                



                                                                                    
                                                                       
                                       





                            
            

                                                                  



















                                                                                 


                                                                                    



















                                                                                 

                                                                   
     

 

























                                                                                      
                       
                  

                  
                                     
                          
                       
                                       

               
                                                       
                    
                             
                 
                                       




                                                
                                                            



                                               
                                                        
                                                                       





























                                                    

 
            


                                                                  

                                                                              



                                                                   
                                                                          





                                                             

                                                       

                                      
                              



                                                                                                   
                     
                                                                    
                  
                                                    


                                                                                



                                                                         
                                                                               

                                        
                                                                        
                     
                 



                                                               

                                           


                                





                                                               
                                              

                                                                         

                                






                                                                 
                     




                                                                                          

                             
                            












                                                                                                                  
                     
                                




                                                            








                                                                                          
                                                                                     

                                                             



                                                             







                                                                                  
                                                                          






                                                                



                                                             








                                                                                               
                                                                          






                                                                


                                
              


                                                                

                                             

                                             




                                                                                        







                                                                 
                                 

                                                             

                              



                                           
                                                                                       











                                                                                                         
                      
                                                        

                                 




                                                                                        







                                                                 
                                 
                                                             

                              



                                           
                                                                                       











                                                                                                         

                      
             

                                          

                            
                                                        
                                              
                                                                          

                                                       
                                                        


                              
                                                                




                                                       
                                                                             


                              
                                                                
              


                                           
                                                              





                                                  
                                                               


                                                        




                                                                   
                                          
                                                                                         

                                                                          


                                                                             
                                                           





                                                                                               

                                  

                                                                        
                     

                                
                 
             





                                                                                







                                                                                          


                                                                                                                  





                                                                                            
                                                                                                                                                
                                                                                           
                                                           












































                                                                                                            
                                                                         
                     
                 
              



                                                        




                                                                                    
                  
                                                                           
                                          














                                                                            

                                  
             


                                                                



                                                                            
                  




                                                                          

                          








                                                                   


                                                                                                   




                                













                                                              



                                        

                                                            
                                                             
                                                     

                                                              
                                                            

                                        
                     






                                                        
                                                                 
                 
             


                                                                         
 
                                                                    

                                                                  
                                                                 

                                                      

                                                       
                                              
                                 











                                                        
 

                                                                                    


                                                      
                                                                         
                    
                                                
             
 
                                                              



                                                

                                                   
         
                                           

                                                             

                            
                                                    


                                                                         

             


                              


                                    
                                            

                                           
                            

                                            

                              
                                            

                                           

















                                            
                                           


                                            
                                           


                                            
                                           















                                            































                                                

     



                                             
                             



                         





                                                                            
                 








                                                  
 
 
                      
                                       




                                        
                     





                                                          

                           
                                                          


                               
                                                            





                                                      
                                                                  
                                                                  




                                                          
                                                              
                
                                                              
         














                                                                    

                                     


                                              

















                                                                                                    
         






                                                                                        










                                                                     
                       
         
                   

               
                                                        
      

                                                                                              







                                                                                
use std::borrow::{Borrow, Cow};
use std::cell::{Ref, RefCell};
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use std::rc::{Rc, Weak};
use std::str::FromStr;
use std::sync::{Arc, Mutex};

use chrono::{Local, Utc};
use filamento::chat::{Chat, Message as ChatMessage};
use filamento::error::{CommandError, DatabaseError};
use filamento::files::Files;
use filamento::presence::{Offline, Presence, PresenceType};
use filamento::{roster::Contact, user::User, UpdateMessage};
use iced::alignment::Horizontal::Right;
use iced::font::{Stretch, Weight};
use iced::futures::{SinkExt, Stream, StreamExt};
use iced::keyboard::{on_key_press, on_key_release, Key, Modifiers};
use iced::theme::palette::{Background, Danger, Extended, Pair, Primary, Secondary, Success};
use iced::theme::{Custom, Palette};
use iced::widget::button::Status;
use iced::widget::text::{Fragment, IntoFragment, Wrapping};
use iced::widget::{
    button, center, checkbox, column, container, horizontal_space, image, mouse_area, opaque, row,
    scrollable, stack, text, text_input, toggler, Column, Svg, Text, Toggler,
};
use iced::Length::{self, Fill, Shrink};
use iced::{color, stream, Color, Element, Font, Subscription, Task, Theme};
use icons::Icon;
use indexmap::{indexmap, IndexMap};
use jid::JID;
use keyring::Entry;
use login_modal::{Creds, LoginModal};
use message_view::MessageView;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::sync::mpsc::Sender;
use tokio::sync::{mpsc, oneshot};
use tokio_stream::wrappers::ReceiverStream;
use tracing::{debug, error, info};
use uuid::Uuid;

mod icons;
mod login_modal;
mod message_view;

#[derive(Serialize, Deserialize, Clone)]
pub struct Config {
    auto_connect: bool,
    storage_dir: Option<String>,
    dburl: Option<String>,
    message_view_config: message_view::Config,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            auto_connect: true,
            storage_dir: None,
            dburl: None,
            message_view_config: message_view::Config::default(),
        }
    }
}

// any object that references another contains an arc to that object, so that items can be garbage-collected by checking reference count
// maybe have a cache which is a set of an enum of reference counted objects, so that when an object is needed it's first cloned from the set, otherwise it is added then cloned. then once an object is no longer needed, it is automatically garbage collected.
// or maybe have the cache items automatically drop themselves at 1 reference? some kind of custom pointer. items in the cache must be easily addressable and updateable.
pub struct Macaw {
    client: Account,
    config: Config,
    presences: HashMap<JID, Presence>,
    subscription_requests: HashSet<MacawUser>,
    new_chat: Option<NewChat>,
    // references chats, users, messages
    open_chat: Option<MessageView>,
    // references users, contacts
    roster: HashMap<JID, MacawContact>,
    // references chats, users, messages
    chats_list: IndexMap<JID, ChatListItem>,
}

#[derive(Debug, Clone)]
pub struct MacawMessage {
    inner: ChatMessage,
    from: MacawUser,
}

impl Deref for MacawMessage {
    type Target = ChatMessage;

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

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

#[derive(Debug, Clone)]
pub struct MacawUser {
    inner: User,
    // contact not needed, as can always query the roster store to get this option.
    // contact: Option<Contact>,
}

impl Deref for MacawUser {
    type Target = User;

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

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

#[derive(Debug, Clone)]
pub struct MacawContact {
    inner: Contact,
    user: User,
}

impl Deref for MacawContact {
    type Target = Contact;

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

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

#[derive(Debug, Clone)]
pub struct MacawChat {
    inner: Chat,
    user: MacawUser,
}

pub struct ChatListItem {
    // references chats
    inner: MacawChat,
    // references users, messages
    latest_message: Option<MacawMessage>,
}

impl Deref for ChatListItem {
    type Target = MacawChat;

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

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

impl Deref for MacawChat {
    type Target = Chat;

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

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

pub struct NewChat;

impl Macaw {
    pub fn new(client: Option<Client>, config: Config) -> Self {
        let account;
        if let Some(client) = client {
            account = Account::LoggedIn(client);
        } else {
            account = Account::LoggedOut(LoginModal::default());
        }

        Self {
            client: account,
            config,
            presences: HashMap::new(),
            subscription_requests: HashSet::new(),
            new_chat: None,
            open_chat: None,
            roster: HashMap::new(),
            chats_list: IndexMap::new(),
        }
    }
}

pub enum Account {
    LoggedIn(Client),
    LoggedOut(LoginModal),
}

impl Account {
    pub fn is_connected(&self) -> bool {
        match self {
            Account::LoggedIn(client) => client.connection_state.is_connected(),
            Account::LoggedOut(login_modal) => false,
        }
    }

    pub fn connection_status(&self) -> String {
        match self {
            Account::LoggedIn(client) => match client.connection_state {
                ConnectionState::Online => "online".to_string(),
                ConnectionState::Connecting => "connecting".to_string(),
                ConnectionState::Offline => "offline".to_string(),
            },
            Account::LoggedOut(login_modal) => "no account".to_string(),
        }
    }
}

#[derive(Clone, Debug)]
pub struct Client {
    client: filamento::Client<Files>,
    files_root: PathBuf,
    jid: JID,
    status: Presence,
    connection_state: ConnectionState,
}

impl Client {
    pub fn is_connected(&self) -> bool {
        self.connection_state.is_connected()
    }

    pub fn files_root(&self) -> &Path {
        &self.files_root
    }
}

#[derive(Clone, Debug)]
pub enum ConnectionState {
    Online,
    Connecting,
    Offline,
}

impl ConnectionState {
    pub fn is_connected(&self) -> bool {
        match self {
            ConnectionState::Online => true,
            ConnectionState::Connecting => false,
            ConnectionState::Offline => false,
        }
    }
}

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

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

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

async fn filamento(
    jid: &JID,
    creds: &Creds,
    cfg: &Config,
) -> (
    (filamento::Client<Files>, mpsc::Receiver<UpdateMessage>),
    PathBuf,
) {
    let filamento;
    if let Some(ref dburl) = cfg.dburl {
        // TODO: have some sort of crash popup for this stuff
        let db_path = dburl.strip_prefix("sqlite://").unwrap_or(&dburl);
        let db_path = PathBuf::from_str(db_path).expect("invalid database path");
        let db = filamento::db::Db::create_connect_and_migrate(db_path)
            .await
            .unwrap();
        let files;
        if let Some(ref dir) = cfg.storage_dir {
            let mut data_dir = PathBuf::from_str(&dir).expect("invalid storage directory path");
            data_dir.push(creds.jid.clone());
            let files_dir = data_dir.join("files");
            files = Files::new(&files_dir);
            if !tokio::fs::try_exists(&files_dir)
                .await
                .expect("could not read storage directory")
            {
                tokio::fs::create_dir_all(&files_dir)
                    .await
                    .expect("could not create file storage directory")
            }
            filamento = (
                filamento::Client::new(jid.clone(), creds.password.to_string(), db, files),
                files_dir,
            );
        } else {
            let mut data_dir = dirs::data_dir().expect(
                "operating system does not support retreiving determining default data dir",
            );
            data_dir.push("macaw");
            data_dir.push(creds.jid.clone());
            data_dir.push("files");
            let files_dir = data_dir;
            files = Files::new(&files_dir);
            if !tokio::fs::try_exists(&files_dir)
                .await
                .expect("could not read storage directory")
            {
                tokio::fs::create_dir_all(&files_dir)
                    .await
                    .expect("could not create file storage directory")
            }
            filamento = (
                filamento::Client::new(jid.clone(), creds.password.to_string(), db, files),
                files_dir,
            );
        }
    } else {
        if let Some(ref dir) = cfg.storage_dir {
            let mut data_dir = PathBuf::from_str(&dir).expect("invalid storage directory path");
            data_dir.push(creds.jid.clone());
            let files_dir = data_dir.join("files");
            let files = Files::new(&files_dir);
            data_dir.push(format!("{}.db", creds.jid.clone()));
            let db = filamento::db::Db::create_connect_and_migrate(data_dir)
                .await
                .unwrap();
            if !tokio::fs::try_exists(&files_dir)
                .await
                .expect("could not read storage directory")
            {
                tokio::fs::create_dir_all(&files_dir)
                    .await
                    .expect("could not create file storage directory")
            }
            filamento = (
                filamento::Client::new(jid.clone(), creds.password.to_string(), db, files),
                files_dir,
            );
        } else {
            let mut data_dir = dirs::data_dir().expect(
                "operating system does not support retreiving determining default data dir",
            );
            data_dir.push("macaw");
            data_dir.push(creds.jid.clone());
            let files_dir = data_dir.join("files");
            let files = Files::new(&files_dir);
            data_dir.push(format!("{}.db", creds.jid.clone()));
            info!("db_path: {:?}", data_dir);
            let db = filamento::db::Db::create_connect_and_migrate(data_dir)
                .await
                .unwrap();
            if !tokio::fs::try_exists(&files_dir)
                .await
                .expect("could not read storage directory")
            {
                tokio::fs::create_dir_all(&files_dir)
                    .await
                    .expect("could not create file storage directory")
            }
            filamento = (
                filamento::Client::new(jid.clone(), creds.password.to_string(), db, files),
                files_dir,
            );
        }
    }
    filamento
}

#[tokio::main]
async fn main() -> iced::Result {
    tracing_subscriber::fmt::init();

    let cfg: Config = confy::load("macaw", None).unwrap_or_default();
    let entry = Entry::new("macaw", "macaw");
    let mut client_creation_error: Option<Error> = None;
    let mut creds: Option<Creds> = None;

    match entry {
        Ok(e) => {
            let result = e.get_password();
            match result {
                Ok(c) => {
                    let result = toml::from_str(&c);
                    match result {
                        Ok(c) => creds = Some(c),
                        Err(e) => {
                            client_creation_error =
                                Some(Error::CredentialsLoad(CredentialsLoadError::Toml(e.into())))
                        }
                    }
                }
                Err(e) => match e {
                    keyring::Error::NoEntry => {}
                    _ => {
                        client_creation_error = Some(Error::CredentialsLoad(
                            CredentialsLoadError::Keyring(e.into()),
                        ))
                    }
                },
            }
        }
        Err(e) => {
            client_creation_error = Some(Error::CredentialsLoad(CredentialsLoadError::Keyring(
                e.into(),
            )))
        }
    }

    let mut client: Option<(
        JID,
        filamento::Client<Files>,
        mpsc::Receiver<UpdateMessage>,
        PathBuf,
    )> = None;
    if let Some(creds) = creds {
        let jid = creds.jid.parse::<JID>();
        match jid {
            Ok(jid) => {
                let ((handle, updates), files_dir) = filamento(&jid, &creds, &cfg).await;
                client = Some((jid, handle, updates, files_dir));
            }
            Err(e) => client_creation_error = Some(Error::CredentialsLoad(e.into())),
        }
    }

    if let Some((jid, luz_handle, update_recv, files_root)) = client {
        let stream = ReceiverStream::new(update_recv);
        let stream = stream.map(|message| Message::Luz(message));
        let task = {
            let luz_handle1 = luz_handle.clone();
            let luz_handle2 = luz_handle.clone();
            if cfg.auto_connect {
                Task::batch([
                    Task::batch([
                        Task::perform(
                            async move { luz_handle1.get_roster_with_users().await },
                            |result| {
                                let roster = result.unwrap();
                                let mut macaw_roster = HashMap::new();
                                for (contact, user) in roster {
                                    macaw_roster.insert(
                                        contact.user_jid.clone(),
                                        MacawContact {
                                            inner: contact,
                                            user,
                                        },
                                    );
                                }
                                Message::Roster(macaw_roster)
                            },
                        ),
                        Task::perform(
                            async move {
                                luz_handle2
                                    .get_chats_ordered_with_latest_messages_and_users()
                                    .await
                            },
                            |chats| {
                                let chats = chats.unwrap();
                                info!("got chats: {:?}", chats);
                                Message::GotChats(chats)
                            },
                        ),
                    ])
                    .chain(Task::done(Message::Connect)),
                    Task::stream(stream),
                ])
            } else {
                debug!("no auto connect");
                Task::batch([
                    Task::perform(
                        async move { luz_handle1.get_roster_with_users().await },
                        |result| {
                            let roster = result.unwrap();
                            let mut macaw_roster = HashMap::new();
                            for (contact, user) in roster {
                                macaw_roster.insert(
                                    contact.user_jid.clone(),
                                    MacawContact {
                                        inner: contact,
                                        user,
                                    },
                                );
                            }
                            Message::Roster(macaw_roster)
                        },
                    ),
                    Task::perform(
                        async move {
                            luz_handle2
                                .get_chats_ordered_with_latest_messages_and_users()
                                .await
                        },
                        |chats| {
                            let chats = chats.unwrap();
                            info!("got chats: {:?}", chats);
                            Message::GotChats(chats)
                        },
                    ),
                    Task::stream(stream),
                ])
            }
        };
        let mut font = Font::with_name("K2D");
        font.weight = Weight::Light;
        // font.stretch = Stretch::Condensed;
        iced::application("Macaw", Macaw::update, Macaw::view)
            .font(include_bytes!("../assets/fonts/Diolce-Regular.ttf"))
            .font(include_bytes!("../assets/fonts/K2D-Italic.ttf"))
            .font(include_bytes!("../assets/fonts/K2D-Thin.ttf"))
            .font(include_bytes!("../assets/fonts/K2D-ExtraBold.ttf"))
            .font(include_bytes!("../assets/fonts/K2D-ExtraLightItalic.ttf"))
            .font(include_bytes!("../assets/fonts/K2D-ExtraLight.ttf"))
            .font(include_bytes!("../assets/fonts/K2D-Light.ttf"))
            .font(include_bytes!("../assets/fonts/K2D-Light.ttf"))
            .font(include_bytes!("../assets/fonts/K2D-BoldItalic.ttf"))
            .font(include_bytes!("../assets/fonts/K2D-MediumItalic.ttf"))
            .font(include_bytes!("../assets/fonts/K2D-ThinItalic.ttf"))
            .font(include_bytes!("../assets/fonts/K2D-Medium.ttf"))
            .font(include_bytes!("../assets/fonts/K2D-Bold.ttf"))
            .font(include_bytes!("../assets/fonts/K2D-Regular.ttf"))
            .font(include_bytes!("../assets/fonts/K2D-ExtraBoldItalic.ttf"))
            .font(include_bytes!("../assets/fonts/K2D-LightItalic.ttf"))
            .font(include_bytes!("../assets/fonts/K2D-SemiBoldItalic.ttf"))
            .font(include_bytes!("../assets/fonts/K2D-SemiBold.ttf"))
            .default_font(font)
            .subscription(subscription)
            .theme(Macaw::theme)
            .run_with(|| {
                (
                    Macaw::new(
                        Some(Client {
                            client: luz_handle,
                            jid,
                            status: Presence {
                                timestamp: Utc::now(),
                                presence: PresenceType::Offline(Offline::default()),
                            },
                            connection_state: ConnectionState::Offline,
                            files_root,
                        }),
                        cfg,
                    ),
                    task,
                )
            })
    } else {
        if let Some(e) = client_creation_error {
            iced::application("Macaw", Macaw::update, Macaw::view)
                .font(include_bytes!("../assets/fonts/Diolce-Regular.otf"))
                .font(include_bytes!("../assets/fonts/K2D-Italic.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-Thin.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-ExtraBold.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-ExtraLightItalic.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-ExtraLight.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-Light.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-Light.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-BoldItalic.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-MediumItalic.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-ThinItalic.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-Medium.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-Bold.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-Regular.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-ExtraBoldItalic.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-LightItalic.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-SemiBoldItalic.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-SemiBold.ttf"))
                .default_font(Font::with_name("K2D"))
                .theme(Macaw::theme)
                .run_with(|| (Macaw::new(None, cfg), Task::done(Message::Error(e))))
        } else {
            iced::application("Macaw", Macaw::update, Macaw::view)
                .font(include_bytes!("../assets/fonts/Diolce-Regular.otf"))
                .font(include_bytes!("../assets/fonts/K2D-Italic.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-Thin.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-ExtraBold.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-ExtraLightItalic.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-ExtraLight.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-Light.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-Light.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-BoldItalic.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-MediumItalic.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-ThinItalic.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-Medium.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-Bold.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-Regular.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-ExtraBoldItalic.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-LightItalic.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-SemiBoldItalic.ttf"))
                .font(include_bytes!("../assets/fonts/K2D-SemiBold.ttf"))
                .default_font(Font::with_name("K2D"))
                .theme(Macaw::theme)
                .run_with(|| (Macaw::new(None, cfg), Task::none()))
        }
    }
}

fn subscription(state: &Macaw) -> Subscription<Message> {
    Subscription::batch([press_subscription(state), release_subscription(state)])
}

fn press_subscription(_state: &Macaw) -> Subscription<Message> {
    on_key_press(handle_key_press)
}

fn handle_key_press(key: Key, r#mod: Modifiers) -> Option<Message> {
    match key {
        Key::Named(iced::keyboard::key::Named::Shift) => Some(Message::ShiftPressed),
        _ => None,
    }
}

fn release_subscription(_state: &Macaw) -> Subscription<Message> {
    on_key_release(handle_key_release)
}

fn handle_key_release(key: Key, r#mod: Modifiers) -> Option<Message> {
    match key {
        Key::Named(iced::keyboard::key::Named::Shift) => Some(Message::ShiftReleased),
        _ => None,
    }
}

#[derive(Debug, Clone)]
pub enum Message {
    ShiftPressed,
    ShiftReleased,
    LoginModal(login_modal::Message),
    ClientCreated(Client),
    Luz(UpdateMessage),
    Roster(HashMap<JID, MacawContact>),
    Connect,
    Disconnect,
    GotChats(Vec<((Chat, User), (ChatMessage, User))>),
    ToggleChat(JID),
    SendMessage(JID, String),
    Error(Error),
    MessageView(message_view::Message),
}

#[derive(Debug, Error, Clone)]
pub enum Error {
    #[error("failed to create Luz client: {0}")]
    ClientCreation(#[from] filamento::error::DatabaseError),
    #[error("failed to save credentials: {0}")]
    CredentialsSave(CredentialsSaveError),
    #[error("failed to load credentials: {0}")]
    CredentialsLoad(CredentialsLoadError),
    #[error("failed to retreive messages for chat {0}")]
    MessageHistory(JID, CommandError<filamento::error::DatabaseError>),
}

#[derive(Debug, Error, Clone)]
pub enum CredentialsSaveError {
    #[error("keyring: {0}")]
    Keyring(Arc<keyring::Error>),
    #[error("toml serialisation: {0}")]
    Toml(#[from] toml::ser::Error),
}

impl From<keyring::Error> for CredentialsSaveError {
    fn from(e: keyring::Error) -> Self {
        Self::Keyring(Arc::new(e))
    }
}

#[derive(Debug, Error, Clone)]
pub enum CredentialsLoadError {
    #[error("keyring: {0}")]
    Keyring(Arc<keyring::Error>),
    #[error("toml serialisation: {0}")]
    Toml(#[from] toml::de::Error),
    #[error("invalid jid: {0}")]
    JID(#[from] jid::ParseError),
}

impl From<keyring::Error> for CredentialsLoadError {
    fn from(e: keyring::Error) -> Self {
        Self::Keyring(Arc::new(e))
    }
}

impl Macaw {
    fn update(&mut self, message: Message) -> Task<Message> {
        match message {
            Message::Luz(update_message) => match update_message {
                UpdateMessage::Online(online, vec) => match &mut self.client {
                    Account::LoggedIn(client) => {
                        client.status = Presence {
                            timestamp: Utc::now(),
                            presence: PresenceType::Online(online),
                        };
                        client.connection_state = ConnectionState::Online;
                        let roster = vec
                            .into_iter()
                            .map(|(contact, user)| {
                                (
                                    contact.user_jid.clone(),
                                    MacawContact {
                                        inner: contact,
                                        user,
                                    },
                                )
                            })
                            .collect();
                        // no need to also update users as any user updates will come in separately
                        self.roster = roster;
                        Task::none()
                    }
                    Account::LoggedOut(login_modal) => Task::none(),
                },
                UpdateMessage::Offline(offline) => {
                    // TODO: update all contacts' presences to unknown (offline)
                    match &mut self.client {
                        Account::LoggedIn(client) => {
                            client.status = Presence {
                                timestamp: Utc::now(),
                                presence: PresenceType::Offline(offline),
                            };
                            client.connection_state = ConnectionState::Offline;
                            Task::none()
                        }
                        Account::LoggedOut(login_modal) => Task::none(),
                    }
                }
                UpdateMessage::RosterUpdate(contact, user) => {
                    self.roster.insert(
                        contact.user_jid.clone(),
                        MacawContact {
                            inner: contact,
                            user,
                        },
                    );
                    Task::none()
                }
                UpdateMessage::RosterDelete(jid) => {
                    self.roster.remove(&jid);
                    Task::none()
                }
                UpdateMessage::Presence { from, presence } => {
                    // TODO: presence handling
                    info!("got presence from {:?} {:?}", from, presence);
                    self.presences.insert(from.as_bare(), presence);
                    Task::none()
                }
                UpdateMessage::Message { to, message, from } => {
                    let message = MacawMessage {
                        inner: message,
                        from: MacawUser { inner: from },
                    };
                    if let Some((chat_jid, mut chat_list_item)) =
                        self.chats_list.shift_remove_entry(&to)
                    {
                        chat_list_item.latest_message = Some(message.clone());
                        self.chats_list.insert_before(0, chat_jid, chat_list_item);
                        if let Some(open_chat) = &mut self.open_chat {
                            if open_chat.chat.user.jid == to {
                                open_chat.update(message_view::Message::Message(message));
                            }
                        }
                    } else {
                        // TODO: get the actual chat from the thing, or send the chat first, from the client side.
                        let chat = Chat {
                            correspondent: to.clone(),
                            have_chatted: false,
                        };
                        let chat_list_item = ChatListItem {
                            inner: MacawChat {
                                inner: chat,
                                user: message.from.clone(),
                            },
                            latest_message: Some(message),
                        };
                        self.chats_list.insert_before(0, to, chat_list_item);
                    }
                    Task::none()
                }
                UpdateMessage::SubscriptionRequest(jid) => {
                    // TODO: subscription requests
                    Task::none()
                }
                UpdateMessage::MessageDelivery { chat, id, delivery } => {
                    if let Some(chat_list_item) = self.chats_list.get_mut(&chat) {
                        if let Some(latest_message) = &mut chat_list_item.latest_message {
                            if latest_message.id == id {
                                latest_message.delivery = Some(delivery)
                            }
                        }
                    }
                    if let Some(open_chat) = &mut self.open_chat {
                        if let Some((message, _)) = open_chat.messages.get_mut(&id) {
                            message.delivery = Some(delivery)
                        }
                    }
                    Task::none()
                }
                UpdateMessage::NickChanged { jid, nick } => {
                    // roster, chats_list, open chat
                    if let Some(contact) = self.roster.get_mut(&jid) {
                        contact.user.nick = nick.clone();
                    }
                    if let Some(chats_list_item) = self.chats_list.get_mut(&jid) {
                        chats_list_item.user.nick = nick.clone()
                    }
                    if let Some(open_chat) = &mut self.open_chat {
                        for (_, (message, _)) in &mut open_chat.messages {
                            if message.from.jid == jid {
                                message.from.nick = nick.clone()
                            }
                        }
                        if open_chat.chat.user.jid == jid {
                            open_chat.chat.user.nick = nick
                        }
                    }
                    Task::none()
                }
                UpdateMessage::AvatarChanged { jid, id } => {
                    // roster, chats_list, open chat
                    if let Some(contact) = self.roster.get_mut(&jid) {
                        contact.user.avatar = id.clone();
                    }
                    if let Some(chats_list_item) = self.chats_list.get_mut(&jid) {
                        chats_list_item.user.avatar = id.clone()
                    }
                    if let Some(open_chat) = &mut self.open_chat {
                        // TODO: consider using an indexmap with two keys for speeding this up?
                        for (_, (message, _)) in &mut open_chat.messages {
                            if message.from.jid == jid {
                                message.from.avatar = id.clone()
                            }
                        }
                        if open_chat.chat.user.jid == jid {
                            open_chat.chat.user.avatar = id
                        }
                    }
                    Task::none()
                }
            },
            // TODO: NEXT
            Message::ClientCreated(client) => {
                self.client = Account::LoggedIn(client.clone());
                let client1 = client.clone();
                let client2 = client.clone();
                if self.config.auto_connect {
                    Task::batch([
                        Task::perform(
                            async move { client1.client.get_roster_with_users().await },
                            |result| {
                                let roster = result.unwrap();
                                let mut macaw_roster = HashMap::new();
                                for (contact, user) in roster {
                                    macaw_roster.insert(
                                        contact.user_jid.clone(),
                                        MacawContact {
                                            inner: contact,
                                            user,
                                        },
                                    );
                                }
                                // TODO: clean this up
                                Message::Roster(macaw_roster)
                            },
                        ),
                        Task::perform(
                            async move {
                                client2
                                    .client
                                    .get_chats_ordered_with_latest_messages_and_users()
                                    .await
                            },
                            |chats| {
                                let chats = chats.unwrap();
                                // let chats: HashMap<JID, (Chat, IndexMap<Uuid, ChatMessage>)> = chats
                                //     .into_iter()
                                //     .map(|chat| (chat.correspondent.clone(), (chat, IndexMap::new())))
                                //     .collect();
                                info!("got chats: {:?}", chats);
                                Message::GotChats(chats)
                            },
                        ),
                    ])
                    .chain(Task::done(Message::Connect))
                } else {
                    Task::batch([
                        Task::perform(
                            async move { client1.client.get_roster_with_users().await },
                            |result| {
                                let roster = result.unwrap();
                                let mut macaw_roster = HashMap::new();
                                for (contact, user) in roster {
                                    macaw_roster.insert(
                                        contact.user_jid.clone(),
                                        MacawContact {
                                            inner: contact,
                                            user,
                                        },
                                    );
                                }
                                Message::Roster(macaw_roster)
                            },
                        ),
                        Task::perform(
                            async move {
                                client2
                                    .client
                                    .get_chats_ordered_with_latest_messages_and_users()
                                    .await
                            },
                            |chats| {
                                let chats = chats.unwrap();
                                // let chats: HashMap<JID, (Chat, IndexMap<Uuid, ChatMessage>)> = chats
                                //     .into_iter()
                                //     .map(|chat| (chat.correspondent.clone(), (chat, IndexMap::new())))
                                //     .collect();
                                info!("got chats: {:?}", chats);
                                Message::GotChats(chats)
                            },
                        ),
                    ])
                }
            }
            Message::Roster(hash_map) => {
                self.roster = hash_map;
                Task::none()
            }
            Message::Connect => match &mut self.client {
                Account::LoggedIn(client) => {
                    client.connection_state = ConnectionState::Connecting;
                    let client = client.client.clone();
                    Task::future(async move {
                        client.connect().await.unwrap();
                    })
                    .discard()
                }
                Account::LoggedOut(login_modal) => Task::none(),
            },
            Message::Disconnect => match &self.client {
                Account::LoggedIn(client) => {
                    let client = client.client.clone();
                    Task::future(async move {
                        client.disconnect(Offline::default()).await.unwrap();
                    })
                    .discard()
                }
                Account::LoggedOut(login_modal) => Task::none(),
            },
            Message::ToggleChat(jid) => {
                match &self.open_chat {
                    Some(message_view) => {
                        if message_view.chat.user.jid == jid {
                            self.open_chat = None;
                            return Task::none();
                        }
                    }
                    None => {}
                }
                if let Some(chat) = self.chats_list.get(&jid) {
                    match &self.client {
                        Account::LoggedIn(client) => {
                            let client = client.clone();
                            self.open_chat = Some(MessageView::new(
                                (*chat).clone(),
                                &self.config,
                                client.files_root.clone(),
                            ));
                            Task::perform(
                                async move { client.get_messages_with_users(jid).await },
                                move |result| {
                                    let message_history = result.unwrap();
                                    let messages = message_history
                                        .into_iter()
                                        .map(|(message, user)| MacawMessage {
                                            inner: message,
                                            from: MacawUser { inner: user },
                                        })
                                        .collect();
                                    Message::MessageView(message_view::Message::MessageHistory(
                                        messages,
                                    ))
                                },
                            )
                        }
                        Account::LoggedOut(login_modal) => Task::none(),
                    }
                } else {
                    Task::none()
                }
            }
            Message::LoginModal(login_modal_message) => match &mut self.client {
                Account::LoggedIn(_client) => Task::none(),
                Account::LoggedOut(login_modal) => {
                    let action = login_modal.update(login_modal_message);
                    match action {
                        login_modal::Action::None => Task::none(),
                        login_modal::Action::CreateClient(jid, password, remember_me) => {
                            let creds = Creds { jid, password };
                            let jid = creds.jid.parse::<JID>();
                            let config = self.config.clone();
                            match jid {
                                Ok(jid) => {
                                    Task::perform(async move {
                                        let (jid, creds, config) = (jid, creds, config);
                                        let ((handle, recv), files_root) = filamento(&jid, &creds, &config).await;
                                        (handle, recv, jid, creds, config, files_root)
                                    }, move |(handle, recv, jid, creds, config, files_root)| {
                                        let creds = creds;
                                        let mut tasks = Vec::new();
                                        tasks.push(Task::done(crate::Message::ClientCreated(
                                            Client {
                                                client: handle,
                                                jid,
                                                status: Presence { timestamp: Utc::now(), presence: PresenceType::Offline(Offline::default()) },
                                                connection_state: ConnectionState::Offline,
                                                files_root,
                                            },
                                        )));
                                        let stream = ReceiverStream::new(recv);
                                        let stream =
                                            stream.map(|message| crate::Message::Luz(message));
                                        tasks.push(Task::stream(stream));

                                        if remember_me {
                                            let entry = Entry::new("macaw", "macaw");
                                            match entry {
                                                Ok(e) => {
                                                    let creds = toml::to_string(&creds);
                                                    match creds {
                                                        Ok(c) => {
                                                            let result = e.set_password(&c);
                                                            if let Err(e) = result {
                                                                tasks.push(Task::done(crate::Message::Error(
                                                                    crate::Error::CredentialsSave(e.into()),
                                                                )));
                                                            }
                                                        }
                                                        Err(e) => tasks.push(Task::done(
                                                            crate::Message::Error(
                                                                crate::Error::CredentialsSave(
                                                                    e.into(),
                                                                ),
                                                            ),
                                                        )),
                                                    }
                                                }
                                                Err(e) => {
                                                    tasks.push(Task::done(crate::Message::Error(
                                                        crate::Error::CredentialsSave(e.into()),
                                                    )))
                                                }
                                            }
                                        }
                                        tasks
                                    }).then(|tasks| Task::batch(tasks))
                                }
                                Err(e) => Task::done(Message::LoginModal(
                                    login_modal::Message::Error(login_modal::Error::InvalidJID),
                                )),
                            }
                        }
                        login_modal::Action::ClientCreated(task) => task,
                    }
                }
            },
            Message::GotChats(chats) => {
                let mut tasks = Vec::new();
                let client = match &self.client {
                    Account::LoggedIn(client) => client,
                    Account::LoggedOut(_) => {
                        // TODO: error into event tracing subscriber
                        error!("no client, cannot retreive chat history for chats");
                        return Task::none();
                    }
                };
                for ((chat, chat_user), (message, message_user)) in chats {
                    let chat = MacawChat {
                        inner: chat,
                        user: MacawUser { inner: chat_user },
                    };
                    let latest_message = MacawMessage {
                        inner: message,
                        from: MacawUser {
                            inner: message_user,
                        },
                    };
                    let chat_list_item = ChatListItem {
                        inner: chat.clone(),
                        latest_message: Some(latest_message),
                    };
                    self.chats_list
                        .insert(chat.correspondent.clone(), chat_list_item);
                }
                Task::batch(tasks)
            }
            Message::SendMessage(jid, body) => {
                let client = match &self.client {
                    Account::LoggedIn(client) => client.clone(),
                    Account::LoggedOut(_) => {
                        error!("cannot send message when no client set up");
                        return Task::none();
                    }
                };
                Task::future(async move {
                    client
                        .send_message(jid, filamento::chat::Body { body })
                        .await
                })
                .discard()
            }
            Message::Error(error) => {
                error!("{}", error);
                Task::none()
            }
            Message::MessageView(message) => {
                if let Some(message_view) = &mut self.open_chat {
                    let action = message_view.update(message);
                    match action {
                        message_view::Action::None => Task::none(),
                        message_view::Action::SendMessage(m) => {
                            Task::done(Message::SendMessage(message_view.chat.user.jid.clone(), m))
                        }
                    }
                } else {
                    Task::none()
                }
            }
            Message::ShiftPressed => {
                info!("shift pressed");
                if let Some(open_chat) = &mut self.open_chat {
                    open_chat.shift_pressed = true;
                }
                Task::none()
            }
            Message::ShiftReleased => {
                info!("shift released");
                if let Some(open_chat) = &mut self.open_chat {
                    open_chat.shift_pressed = false;
                }
                Task::none()
            }
        }
    }

    fn view(&self) -> Element<Message> {
        let mut ui: Element<Message> = {
            let mut chats_list: Column<Message> = column![];
            if let Account::LoggedIn(client) = &self.client {
                for (jid, chat) in &self.chats_list {
                    let mut open = false;
                    if let Some(open_chat) = &self.open_chat {
                        if open_chat.chat.user.jid == *jid {
                            open = true;
                        }
                    }
                    let chat_list_item = chat_list_item(
                        &self.presences,
                        &self.roster,
                        client.files_root(),
                        chat,
                        open,
                    );
                    chats_list = chats_list.push(chat_list_item);
                }
            }
            let chats_list = scrollable(chats_list.spacing(8).padding(8))
                .spacing(1)
                .height(Fill);

            let connection_status = self.client.connection_status();
            let client_jid: Cow<'_, str> = match &self.client {
                Account::LoggedIn(client) => (&client.jid).into(),
                Account::LoggedOut(_) => Cow::from("no account"),
                // map(|client| (&client.jid).into());
            };
            let connected = self.client.is_connected();

            let account_view = container(row![
                text(client_jid),
                horizontal_space(),
                text(connection_status),
                horizontal_space().width(8),
                toggler(connected).on_toggle(|connect| {
                    if connect {
                        Message::Connect
                    } else {
                        Message::Disconnect
                    }
                })
            ])
            .padding(8);

            // TODO: config width/resizing
            let sidebar = column![chats_list, account_view].height(Fill).width(300);

            let message_view;
            if let Some(open_chat) = &self.open_chat {
                message_view = open_chat.view().map(Message::MessageView)
            } else {
                message_view = column![].into();
            }

            row![sidebar, container(message_view).width(Fill)]
        }
        .into();

        if let Some(new_chat) = &self.new_chat {
            // TODO: close new chat window
            ui = modal(ui, text("new chat"), None);
        }
        // temporarily center to fill space
        // let ui = center(ui).into();
        let ui = container(ui).center_x(Fill).center_y(Fill);

        match &self.client {
            Account::LoggedIn(_client) => ui.into(),
            Account::LoggedOut(login_modal) => {
                let signup = login_modal.view().map(Message::LoginModal);
                modal(ui, signup, None)
            }
        }
    }

    fn theme(&self) -> Theme {
        let extended = Extended {
            background: Background {
                base: Pair {
                    color: color!(0x503e34),
                    text: color!(0xdcdcdc),
                },
                weak: Pair {
                    color: color!(0x392c25),
                    text: color!(0xdcdcdc),
                },
                strong: Pair {
                    color: color!(0x293f2e),
                    text: color!(0xdcdcdc),
                },
            },
            primary: Primary {
                base: Pair {
                    color: color!(0x2b33b4),
                    text: color!(0xdcdcdc),
                },
                weak: Pair {
                    color: color!(0x4D4A5E),
                    text: color!(0xdcdcdc),
                },
                strong: Pair {
                    color: color!(0x2b33b4),
                    text: color!(0xdcdcdc),
                },
            },
            secondary: Secondary {
                base: Pair {
                    color: color!(0xffce07),
                    text: color!(0xdcdcdc),
                },
                weak: Pair {
                    color: color!(0xffce07),
                    text: color!(0xdcdcdc),
                },
                strong: Pair {
                    color: color!(0xffce07),
                    text: color!(0xdcdcdc),
                },
            },
            success: Success {
                base: Pair {
                    color: color!(0x14802E),
                    text: color!(0xdcdcdc),
                },
                weak: Pair {
                    color: color!(0x14802E),
                    text: color!(0xdcdcdc),
                },
                strong: Pair {
                    color: color!(0x14802E),
                    text: color!(0xdcdcdc),
                },
            },
            danger: Danger {
                base: Pair {
                    color: color!(0xC1173C),
                    text: color!(0xdcdcdc),
                },
                weak: Pair {
                    color: color!(0xC1173C),
                    text: color!(0xdcdcdc),
                },
                strong: Pair {
                    color: color!(0xC1173C),
                    text: color!(0xdcdcdc),
                },
            },
            is_dark: true,
        };
        Theme::Custom(Arc::new(Custom::with_fn(
            "macaw".to_string(),
            Palette::DARK,
            |_| extended,
        )))
        // Theme::Custom(Arc::new(Custom::new(
        //     "macaw".to_string(),
        //     Palette {
        //         background: color!(0x392c25),
        //         text: color!(0xdcdcdc),
        //         primary: color!(0x2b33b4),
        //         success: color!(0x14802e),
        //         warning: color!(0xffce07),
        //         danger: color!(0xc1173c),
        //     },
        // )))
    }
}

fn modal<'a, Message>(
    base: impl Into<Element<'a, Message>>,
    content: impl Into<Element<'a, Message>>,
    on_blur: Option<Message>,
) -> Element<'a, Message>
where
    Message: Clone + 'a,
{
    let mut mouse_area = mouse_area(center(opaque(content)).style(|_theme| {
        container::Style {
            background: Some(
                Color {
                    a: 0.8,
                    ..Color::BLACK
                }
                .into(),
            ),
            ..container::Style::default()
        }
    })); // .on_press(on_blur)
    if let Some(on_blur) = on_blur {
        mouse_area = mouse_area.on_press(on_blur)
    }
    stack![base.into(), opaque(mouse_area)].into()
}

fn chat_list_item<'a>(
    presences: &HashMap<JID, Presence>,
    roster: &HashMap<JID, MacawContact>,
    file_root: &'a Path,
    chat_list_item: &'a ChatListItem,
    open: bool,
) -> Element<'a, Message> {
    let name: String;
    if let Some(Some(contact_name)) = roster
        .get(chat_list_item.correspondent())
        .map(|contact| &contact.name)
    {
        name = contact_name.clone()
    } else if let Some(nick) = &chat_list_item.user.nick {
        name = nick.clone()
    } else {
        name = chat_list_item.correspondent().to_string();
    }

    let avatar: Option<String>;
    if let Some(user_avatar) = &chat_list_item.user.avatar {
        avatar = Some(user_avatar.clone())
    } else {
        avatar = None
    }

    let latest_message_text: Option<(String, String)>;
    if let Some(latest_message) = &chat_list_item.latest_message {
        let message = latest_message.body.body.replace("\n", " ");
        let date = latest_message.timestamp.naive_local();
        let now = Local::now().naive_local();
        let timeinfo;
        if date.date() == now.date() {
            // TODO: localisation/config
            timeinfo = date.time().format("%H:%M").to_string()
        } else {
            timeinfo = date.date().format("%d/%m").to_string()
        }
        latest_message_text = Some((message, timeinfo));
        // content = content.push(
        //     row![
        //         container(text(message).wrapping(Wrapping::None))
        //             .clip(true)
        //             .width(Fill),
        //         timeinfo
        //     ]
        //     .spacing(8)
        //     .width(Fill),
        // );
    } else {
        latest_message_text = None;
    }

    let mut avatar_stack = stack([]);
    if let Some(avatar) = avatar {
        let mut path = file_root.join(avatar);
        path.set_extension("jpg");
        info!("got avatar: {:?}", path);
        avatar_stack = avatar_stack.push(image(path).width(48).height(48));
    }
    let mut status_icon: Option<Icon> = None;
    if let Some(presence) = presences.get(&chat_list_item.user.jid) {
        debug!("found a presence");
        match &presence.presence {
            PresenceType::Online(online) => match online.show {
                Some(s) => match s {
                    filamento::presence::Show::Away => status_icon = Some(Icon::Away16Color),
                    filamento::presence::Show::Chat => status_icon = Some(Icon::Bubble16Color),
                    filamento::presence::Show::DoNotDisturb => status_icon = Some(Icon::Dnd16Color),
                    filamento::presence::Show::ExtendedAway => {
                        status_icon = Some(Icon::Away16Color)
                    }
                },
                None => status_icon = Some(Icon::Bubble16Color),
            },
            PresenceType::Offline(offline) => {}
        }
    }
    if let Some(status_icon) = status_icon {
        avatar_stack = avatar_stack.push(Into::<Svg>::into(status_icon));
    }
    let content: Element<Message> = if let Some((message, time)) = latest_message_text {
        row![
            avatar_stack,
            column![
                text(name),
                row![
                    container(text(message).wrapping(Wrapping::None))
                        .clip(true)
                        .width(Fill),
                    text(time)
                ]
                .spacing(8)
                .width(Fill)
            ]
            .spacing(8)
        ]
        .spacing(8)
        .into()
    } else {
        row![avatar_stack, text(name)].spacing(8).into()
    };
    let mut button =
        button(content).on_press(Message::ToggleChat(chat_list_item.correspondent().clone()));
    if open {
        button = button.style(|theme: &Theme, status| {
            let palette = theme.extended_palette();
            button::Style::default().with_background(palette.primary.weak.color)
        });
    }
    button.width(Fill).into()
}