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

                                

                       
                   
 
                         



                                                            
                                       
                                                
                                                                   



                                                                             
                                 
                                                           
                   

                                                                                           
  
                                       
                                                                     
                                   
             
                   
                                     
                              
                                    
                     
                                 
                                           
                           

               
                
                 
 
                                        

                       

                                
                                              



                          



                               
                                                                 
         

     
 
                  
                    
                   


                                      
                                                      
                                        
                                   


                              

                   
            
                                                                



                                                
                                                                



                            
                   


                                      
                                   
                                                  

                            





                     
                          

 



















                                                                                

                       
                              
             
























                                                 








                                                  
                                    



                                      

 





                                                         



                                                                                 
                                                                       

                      
                                                                                        




                                                                                            
                                                                        

                      
                                                                                        








                                                                                                 
                                                                        

                      
                                                                                        
     
             

 

                                 

                                    
                                                                     


































                                                                                                  
                                                                                           



                                           
                                                                            
                                                      



                                                                                     



                                                                 
                    

                                                 
                                 




























                                                                                               
                    








                                                                                           







                                                                                                  

                                         

             
                                                              
                                       







                                               
                                                        



                                                                                    
                                                                       





                            
            






                                                                                    
     

 

























                                                                                      
                       
                  

                  
                                     
                          



                                  
                                       
                                                         
                    
                             
                 
                                       




                                                
                                                            



                                               
                                                        
                                                                       





























                                                    

 
            






                                                                  

                                                                              



                                                                   
                                                                          





                                                                             
                     
                                                                    
                  
                                                    


                                                                                



                                                                         
                                                                               

                                        
                                                                        
                     





















                                                                               



                                                                  
                                                                                       




                                                                                          



                                                      
                                                                                        
                                                                               







                                                            


                                                                

                                             









                                                                                                  
















                                                                                                         
                      
                                                        









                                                                                                  
















                                                                                                         

                      




                                          
                                                        
                                              
                                                                          

                                                       
                                               


                              
                                                                




                                                       
                                                                    


                              
                                                                
              









                                                    
                                                                                   















                                                                                                  
             





                                                                                







                                                                                          
                                                                                                    







                                                                                            
                                                                                                                                                
                                                                                           












































                                                                                                            
                                                                         
                     
                 
              



                                                        




                                                                                    

                                   
                              
















                                                                                              












                                                                      



                                                                      
                                                                                   
                 

                            


                                                                



                                                                            
                  




                                                                          

                          
















                                                                                         













                                                              



                                        

                                                            

                                                              

                                                          
                                    

                     

                                                                                
             


                                                                         
 
                                                                    

                                                                  
                                                                 

                                                      

                                                       
                                              
                                 











                                                        
 

                                                                                    


                                                      
                                                                         
                    
                                                
             
 
                                                              



                                                

                                                   
         
                                           

                                                             

                            
                                                    


                                                                         

             


                              














































































































                                                

     



                                             
                             



                         





                                                                            
                 








                                                  
 







                                                                                     
                                                                  










                                                                    
                                                                 
















                                                                                               
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::ops::{Deref, DerefMut};
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;

use chrono::{Local, Utc};
use filamento::chat::{Chat, Message as ChatMessage};
use filamento::error::CommandError;
use filamento::presence::{Offline, Presence, PresenceType};
use filamento::{roster::Contact, user::User, UpdateMessage};
use iced::alignment::Horizontal::Right;
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, Warning,
};
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, mouse_area, opaque, row,
    scrollable, stack, text, text_input, toggler, Column, Text, Toggler,
};
use iced::Length::{self, Fill, Shrink};
use iced::{color, stream, Color, Element, Subscription, Task, Theme};
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, oneshot};
use tokio_stream::wrappers::ReceiverStream;
use tracing::{error, info};
use uuid::Uuid;

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

pub struct Macaw {
    client: Account,
    config: Config,
    roster: HashMap<JID, Contact>,
    users: HashMap<JID, User>,
    presences: HashMap<JID, Presence>,
    chats: IndexMap<JID, (Chat, Option<ChatMessage>)>,
    subscription_requests: HashSet<JID>,
    open_chat: Option<MessageView>,
    new_chat: Option<NewChat>,
}

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,
            roster: HashMap::new(),
            users: HashMap::new(),
            presences: HashMap::new(),
            chats: IndexMap::new(),
            subscription_requests: HashSet::new(),
            open_chat: None,
            new_chat: None,
        }
    }
}

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,
    jid: JID,
    status: Presence,
    connection_state: ConnectionState,
}

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

#[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;

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

async fn filamento(
    jid: &JID,
    creds: &Creds,
    cfg: &Config,
) -> (filamento::Client, mpsc::Receiver<UpdateMessage>) {
    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();
        filamento = filamento::Client::new(jid.clone(), creds.password.to_string(), db);
    } 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());
        data_dir.push(creds.jid.clone());
        data_dir.set_extension("db");
        let db = filamento::db::Db::create_connect_and_migrate(data_dir)
            .await
            .unwrap();
        filamento = filamento::Client::new(jid.clone(), creds.password.to_string(), db);
    } 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(creds.jid.clone());
        // TODO: better lol
        data_dir.set_extension("db");
        info!("db_path: {:?}", data_dir);
        let db = filamento::db::Db::create_connect_and_migrate(data_dir)
            .await
            .unwrap();
        filamento = filamento::Client::new(jid.clone(), creds.password.to_string(), db);
    }
    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, mpsc::Receiver<UpdateMessage>)> = None;
    if let Some(creds) = creds {
        let jid = creds.jid.parse::<JID>();
        match jid {
            Ok(jid) => {
                let (handle, updates) = filamento(&jid, &creds, &cfg).await;
                client = Some((jid, handle, updates));
            }
            Err(e) => client_creation_error = Some(Error::CredentialsLoad(e.into())),
        }
    }

    if let Some((jid, luz_handle, update_recv)) = 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().await },
                                |result| {
                                    let roster = result.unwrap();
                                    let mut macaw_roster = HashMap::new();
                                    for contact in roster {
                                        macaw_roster.insert(contact.user_jid.clone(), contact);
                                    }
                                    Message::Roster(macaw_roster)
                                },
                            ),
                            Task::perform(
                                async move {
                                    luz_handle2.get_chats_ordered_with_latest_messages().await
                                },
                                |chats| {
                                    let chats = chats.unwrap();
                                    info!("got chats: {:?}", chats);
                                    Message::GotChats(chats)
                                },
                            ),
                        ])
                        .chain(Task::done(Message::Connect)),
                        Task::stream(stream),
                    ],
                )
            } else {
                Task::batch([
                    Task::perform(async move { luz_handle1.get_roster().await }, |result| {
                        let roster = result.unwrap();
                        let mut macaw_roster = HashMap::new();
                        for contact in roster {
                            macaw_roster.insert(contact.user_jid.clone(), contact);
                        }
                        Message::Roster(macaw_roster)
                    }),
                    Task::perform(
                        async move { luz_handle2.get_chats_ordered_with_latest_messages().await },
                        |chats| {
                            let chats = chats.unwrap();
                            info!("got chats: {:?}", chats);
                            Message::GotChats(chats)
                        },
                    ),
                    Task::stream(stream),
                ])
            }
        };
        iced::application("Macaw", Macaw::update, Macaw::view)
            .subscription(subscription)
            .theme(Macaw::theme)
            .run_with(|| {
                (
                    Macaw::new(
                        Some(Client {
                            client: luz_handle,
                            // TODO:
                            jid,
                            // TODO: store cached status
                            status: Presence {
                                timestamp: Utc::now(),
                                presence: PresenceType::Offline(Offline::default()),
                            },
                            connection_state: ConnectionState::Offline,
                        }),
                        cfg,
                    ),
                    task,
                )
            })
    } else {
        if let Some(e) = client_creation_error {
            iced::application("Macaw", Macaw::update, Macaw::view)
                .run_with(|| (Macaw::new(None, cfg), Task::done(Message::Error(e))))
        } else {
            iced::application("Macaw", Macaw::update, Macaw::view)
                .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, Contact>),
    Connect,
    Disconnect,
    GotChats(Vec<(Chat, ChatMessage)>),
    GotMessageHistory(Chat, IndexMap<Uuid, ChatMessage>),
    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::Error(error) => {
                    tracing::error!("Luz error: {:?}", error);
                    Task::none()
                }
                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 mut roster = HashMap::new();
                        for contact in vec {
                            roster.insert(contact.user_jid.clone(), contact);
                        }
                        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::FullRoster(vec) => {
                    let mut macaw_roster = HashMap::new();
                    for contact in vec {
                        macaw_roster.insert(contact.user_jid.clone(), contact);
                    }
                    self.roster = macaw_roster;
                    Task::none()
                }
                UpdateMessage::RosterUpdate(contact) => {
                    self.roster.insert(contact.user_jid.clone(), contact);
                    Task::none()
                }
                UpdateMessage::RosterDelete(jid) => {
                    self.roster.remove(&jid);
                    Task::none()
                }
                UpdateMessage::Presence { from, presence } => {
                    self.presences.insert(from, presence);
                    Task::none()
                }
                UpdateMessage::Message { to, message } => {
                    if let Some((chat_jid, (chat, old_message))) =
                        self.chats.shift_remove_entry(&to)
                    {
                        self.chats
                            .insert_before(0, chat_jid, (chat, Some(message.clone())));
                        if let Some(open_chat) = &mut self.open_chat {
                            if open_chat.jid == to {
                                open_chat.update(message_view::Message::Message(message));
                            }
                        }
                    } else {
                        let chat = Chat {
                            correspondent: to.clone(),
                        };
                        let message_history = indexmap! {message.id => message.clone()};
                        self.chats.insert_before(0, to, (chat, Some(message)));
                    }
                    Task::none()
                }
                UpdateMessage::SubscriptionRequest(jid) => {
                    // TODO: subscription requests
                    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().await }, |result| {
                            let roster = result.unwrap();
                            let mut macaw_roster = HashMap::new();
                            for contact in roster {
                                macaw_roster.insert(contact.user_jid.clone(), contact);
                            }
                            Message::Roster(macaw_roster)
                        }),
                        Task::perform(
                            async move {
                                client2
                                    .client
                                    .get_chats_ordered_with_latest_messages()
                                    .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().await }, |result| {
                            let roster = result.unwrap();
                            let mut macaw_roster = HashMap::new();
                            for contact in roster {
                                macaw_roster.insert(contact.user_jid.clone(), contact);
                            }
                            Message::Roster(macaw_roster)
                        }),
                        Task::perform(
                            async move {
                                client2
                                    .client
                                    .get_chats_ordered_with_latest_messages()
                                    .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;
                    })
                    .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;
                    })
                    .discard()
                }
                Account::LoggedOut(login_modal) => Task::none(),
            },
            Message::ToggleChat(jid) => {
                match &self.open_chat {
                    Some(message_view) => {
                        if message_view.jid == jid {
                            self.open_chat = None;
                            return Task::none();
                        }
                    }
                    None => {}
                }
                self.open_chat = Some(MessageView::new(jid.clone(), &self.config));
                let jid1 = jid.clone();
                match &self.client {
                    Account::LoggedIn(client) => {
                        let client = client.clone();
                        Task::perform(
                            async move { client.get_messages(jid1).await },
                            move |result| match result {
                                Ok(h) => {
                                    Message::MessageView(message_view::Message::MessageHistory(h))
                                }
                                Err(e) => Message::Error(Error::MessageHistory(jid.clone(), e)),
                            },
                        )
                    }
                    Account::LoggedOut(login_modal) => 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) = filamento(&jid, &creds, &config).await;
                                        (handle, recv, jid, creds, config)
                                    }, move |(handle, recv, jid, creds, config)| {
                                        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,
                                            },
                                        )));
                                        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 in chats {
                    self.chats
                        // TODO: could have a chat with no messages, bad database state
                        .insert(chat.0.correspondent.clone(), (chat.0.clone(), Some(chat.1)));
                    // let client = client.clone();
                    // let correspondent = chat.correspondent.clone();
                    // tasks.push(Task::perform(
                    //     // TODO: don't get the entire message history LOL
                    //     async move { (chat, client.get_messages(correspondent).await) },
                    //     |result| {
                    //         let messages: IndexMap<Uuid, ChatMessage> = result
                    //             .1
                    //             .unwrap()
                    //             .into_iter()
                    //             .map(|message| (message.id.clone(), message))
                    //             .collect();
                    //         Message::GotMessageHistory(result.0, messages)
                    //     },
                    // ))
                }
                Task::batch(tasks)
                // .then(|chats| {
                //     let tasks = Vec::new();
                //     for key in chats.keys() {
                //         let client = client.client.clone();
                //         tasks.push(Task::future(async {
                //             client.get_messages(key.clone()).await;
                //         }));
                //     }
                //     Task::batch(tasks)
                // }),
            }
            Message::GotMessageHistory(chat, mut message_history) => {
                // TODO: don't get the entire message history LOL
                if let Some((_id, message)) = message_history.pop() {
                    self.chats
                        .insert(chat.correspondent.clone(), (chat, Some(message)));
                }
                Task::none()
            }
            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.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![];
            for (jid, (chat, latest_message)) in &self.chats {
                let mut open = false;
                if let Some(open_chat) = &self.open_chat {
                    if open_chat.jid == *jid {
                        open = true;
                    }
                }
                let chat_list_item = chat_list_item(chat, latest_message, 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!(0x392c25),
                    text: color!(0xdcdcdc),
                },
                weakest: Pair {
                    color: color!(0xdcdcdc),
                    text: color!(0x392c25),
                },
                weak: Pair {
                    color: color!(0xdcdcdc),
                    text: color!(0x392c25),
                },
                strong: Pair {
                    color: color!(0x364b3b),
                    text: color!(0xdcdcdc),
                },
                strongest: Pair {
                    color: color!(0x364b3b),
                    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!(0x000000),
                },
                weak: Pair {
                    color: color!(0xffce07),
                    text: color!(0x000000),
                },
                strong: Pair {
                    color: color!(0xffce07),
                    text: color!(0x000000),
                },
            },
            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),
                },
            },
            warning: Warning {
                base: Pair {
                    color: color!(0xFF9D00),
                    text: color!(0x000000),
                },
                weak: Pair {
                    color: color!(0xFF9D00),
                    text: color!(0x000000),
                },
                strong: Pair {
                    color: color!(0xFF9D00),
                    text: color!(0x000000),
                },
            },
            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>(
    chat: &'a Chat,
    latest_message: &'a Option<ChatMessage>,
    open: bool,
) -> Element<'a, Message> {
    let mut content: Column<Message> = column![text(chat.correspondent.to_string())];
    if let Some(latest_message) = 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 = text(date.time().format("%H:%M").to_string())
        } else {
            timeinfo = text(date.date().format("%d/%m").to_string())
        }
        content = content.push(
            row![
                container(text(message).wrapping(Wrapping::None))
                    .clip(true)
                    .width(Fill),
                timeinfo
            ]
            .spacing(8)
            .width(Fill),
        );
    }
    let mut button = button(content).on_press(Message::ToggleChat(chat.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()
}