aboutsummaryrefslogblamecommitdiffstats
path: root/src/main.rs
blob: ed6ce1bd7e85d251814cee2a5ef21d6f70660c36 (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 iced::futures::{SinkExt, Stream, StreamExt};
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};
use iced::widget::{
    button, center, checkbox, column, container, mouse_area, opaque, row, scrollable, stack, text,
    text_input, Column, Text, Toggler,
};
use iced::Length::Fill;
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 luz::chat::{Chat, Message as ChatMessage};
use luz::error::CommandError;
use luz::presence::{Offline, Presence};
use luz::CommandMessage;
use luz::{roster::Contact, user::User, LuzHandle, UpdateMessage};
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>,
}

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

pub struct Macaw {
    client: Account,
    config: Config,
    roster: HashMap<JID, Contact>,
    users: HashMap<JID, User>,
    presences: HashMap<JID, Presence>,
    chats: IndexMap<JID, (Chat, 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),
}

#[derive(Clone, Debug)]
pub struct Client {
    client: LuzHandle,
    jid: JID,
    connection_status: Presence,
}

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

impl Deref for Client {
    type Target = LuzHandle;

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

async fn luz(jid: &JID, creds: &Creds, cfg: &Config) -> (LuzHandle, mpsc::Receiver<UpdateMessage>) {
    let luz;
    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 = luz::db::Db::create_connect_and_migrate(db_path)
            .await
            .unwrap();
        luz = LuzHandle::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 = luz::db::Db::create_connect_and_migrate(data_dir)
            .await
            .unwrap();
        luz = LuzHandle::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 = luz::db::Db::create_connect_and_migrate(data_dir)
            .await
            .unwrap();
        luz = LuzHandle::new(jid.clone(), creds.password.to_string(), db);
    }
    luz
}

#[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, LuzHandle, mpsc::Receiver<UpdateMessage>)> = None;
    if let Some(creds) = creds {
        let jid = creds.jid.parse::<JID>();
        match jid {
            Ok(jid) => {
                let (handle, updates) = luz(&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().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().await }, |chats| {
                        let chats = chats.unwrap();
                        info!("got chats: {:?}", chats);
                        Message::GotChats(chats)
                    }),
                    Task::stream(stream),
                ])
            }
        };
        iced::application("Macaw", Macaw::update, Macaw::view)
            .theme(Macaw::theme)
            .run_with(|| {
                (
                    Macaw::new(
                        Some(Client {
                            client: luz_handle,
                            // TODO:
                            jid,
                            connection_status: Presence::Offline(Offline::default()),
                        }),
                        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()))
        }
    }
}

#[derive(Debug, Clone)]
pub enum Message {
    LoginModal(login_modal::Message),
    ClientCreated(Client),
    Luz(UpdateMessage),
    Roster(HashMap<JID, Contact>),
    Connect,
    Disconnect,
    OpenChat(JID),
    GotChats(Vec<Chat>),
    GotMessageHistory(Chat, IndexMap<Uuid, ChatMessage>),
    CloseChat(JID),
    MessageCompose(String),
    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] luz::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<luz::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.connection_status = Presence::Online(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.connection_status = Presence::Offline(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, 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, 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().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().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 &self.client {
                Account::LoggedIn(client) => {
                    let client = client.client.clone();
                    Task::future(async move {
                        client.send(CommandMessage::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
                            .send(CommandMessage::Disconnect(Offline::default()))
                            .await;
                    })
                    .discard()
                }
                Account::LoggedOut(login_modal) => Task::none(),
            },
            Message::OpenChat(jid) => {
                self.open_chat = Some(MessageView::new(jid.clone()));
                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) = luz(&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,
                                                connection_status: Presence::Offline(
                                                    Offline::default(),
                                                ),
                                            },
                                        )));
                                        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 {
                    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, message));
                }
                Task::none()
            }
            Message::CloseChat(jid) => {
                self.open_chat = None;
                Task::none()
            }
            Message::MessageCompose(m) => {
                if let Some(open_chat) = &mut self.open_chat {
                    open_chat.new_message = m;
                }
                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, luz::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()
                }
            }
        }
    }

    fn view(&self) -> Element<Message> {
        let mut ui: Element<Message> = {
            let mut chats_list: Column<Message> = column![];
            for (jid, chat) in &self.chats {
                let cow_jid: Cow<'_, str> = (jid).into();
                let mut toggler: Toggler<Message> = iced::widget::toggler(false);
                if let Some(open_chat) = &self.open_chat {
                    if open_chat.jid == *jid {
                        toggler = iced::widget::toggler(true)
                    }
                }
                let toggler = toggler
                    .on_toggle(|open| {
                        if open {
                            Message::OpenChat(jid.clone())
                        } else {
                            Message::CloseChat(jid.clone())
                        }
                    })
                    .label(cow_jid);
                chats_list = chats_list.push(toggler);
            }
            let chats_list = scrollable(chats_list).height(Fill);

            let connection_status = match &self.client {
                Account::LoggedIn(client) => match &client.connection_status {
                    Presence::Online(_online) => "online",
                    Presence::Offline(_offline) => "disconnected",
                },
                Account::LoggedOut(_) => "disconnected",
            };
            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 account_view = row![
                text(client_jid),
                text(connection_status),
                button("connect").on_press(Message::Connect),
                button("disconnect").on_press(Message::Disconnect)
            ];

            let sidebar = column![chats_list, account_view].height(Fill);

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

            // old

            // let mut contacts: Vec<Element<Message>> = Vec::new();
            // for (_, contact) in &self.roster {
            //     let jid: Cow<'_, str> = (&contact.user_jid).into();
            //     contacts.push(
            //         button(text(jid))
            //             .on_press(Message::OpenChat(contact.user_jid.clone()))
            //             .into(),
            //     );
            // }
        }
        .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()
}