aboutsummaryrefslogblamecommitdiffstats
path: root/src/main.rs
blob: e3d3332908ef3637ea04507184480cb1c22be3cc (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 iced::futures::{SinkExt, Stream, StreamExt};
use iced::widget::button::Status;
use iced::widget::text::{Fragment, IntoFragment};
use iced::widget::{
    button, center, column, container, mouse_area, opaque, row, scrollable, stack, text,
    text_input, Column, Text, Toggler,
};
use iced::Length::Fill;
use iced::{stream, Color, Element, Subscription, Task, Theme};
use indexmap::{indexmap, IndexMap};
use jid::JID;
use keyring::Entry;
use luz::chat::{Chat, Message as ChatMessage};
use luz::presence::{Offline, Presence};
use luz::CommandMessage;
use luz::{roster::Contact, user::User, LuzHandle, UpdateMessage};
use tokio::sync::{mpsc, oneshot};
use tokio_stream::wrappers::ReceiverStream;
use tracing::info;
use uuid::Uuid;

pub struct Config {
    auto_connect: bool,
}

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

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

pub struct OpenChat {
    jid: JID,
    new_message: String,
}

pub struct NewChat;

pub struct Creds {
    jid: String,
    password: String,
}

impl Macaw {
    pub fn new(client: Option<Client>) -> Self {
        let account;
        if let Some(client) = client {
            account = Account::LoggedIn(client);
        } else {
            account = Account::LoggedOut {
                jid: "".to_string(),
                password: "".to_string(),
                error: None,
            };
        }

        Self {
            client: account,
            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 {
        jid: String,
        password: String,
        error: Option<Error>,
    },
}

#[derive(Debug, Clone)]
pub enum Error {
    InvalidJID(String),
    DatabaseConnection,
}

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

fn main() -> iced::Result {
    tracing_subscriber::fmt::init();

    let client: Option<(JID, LuzHandle, mpsc::Receiver<UpdateMessage>)> = None;

    if let Some((jid, luz_handle, update_recv)) = client {
        let stream = ReceiverStream::new(update_recv);
        let stream = stream.map(|message| Message::Luz(message));
        iced::application("Macaw", Macaw::update, Macaw::view).run_with(|| {
            (
                Macaw::new(Some(Client {
                    client: luz_handle,
                    // TODO:
                    jid,
                    connection_status: Presence::Offline(Offline::default()),
                })),
                // TODO: autoconnect config
                Task::stream(stream),
            )
        })
    } else {
        iced::application("Macaw", Macaw::update, Macaw::view)
            .run_with(|| (Macaw::new(None), Task::none()))
    }
}

#[derive(Debug, Clone)]
enum Message {
    LoginModal(LoginModalMessage),
    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),
}

#[derive(Debug, Clone)]
enum LoginModalMessage {
    JID(String),
    Password(String),
    Submit,
    Error(Error),
}

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 {
                        jid,
                        password,
                        error,
                    } => 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 {
                            jid,
                            password,
                            error,
                        } => 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, message_history)) = self.chats.get_mut(&to) {
                        message_history.insert(message.id, message);
                    } else {
                        let chat = Chat {
                            correspondent: to.clone(),
                        };
                        let message_history = indexmap! {message.id => message};
                        self.chats.insert(to, (chat, message_history));
                    }
                    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();
                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();
                        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 {
                    jid,
                    password,
                    error,
                } => 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 {
                    jid,
                    password,
                    error,
                } => Task::none(),
            },
            Message::OpenChat(jid) => {
                self.open_chat = Some(OpenChat {
                    jid,
                    new_message: String::new(),
                });
                Task::none()
            }
            Message::LoginModal(login_modal_message) => match login_modal_message {
                LoginModalMessage::JID(j) => match &mut self.client {
                    Account::LoggedIn(_client) => Task::none(),
                    Account::LoggedOut {
                        jid,
                        password,
                        error,
                    } => {
                        *jid = j;
                        Task::none()
                    }
                },
                LoginModalMessage::Password(p) => match &mut self.client {
                    Account::LoggedIn(_client) => Task::none(),
                    Account::LoggedOut {
                        jid,
                        password,
                        error,
                    } => {
                        *password = p;
                        Task::none()
                    }
                },
                LoginModalMessage::Submit => match &self.client {
                    Account::LoggedIn(_client) => Task::none(),
                    Account::LoggedOut {
                        jid: jid_str,
                        password,
                        error,
                    } => {
                        info!("submitting login");
                        let jid_str = jid_str.clone();
                        let password = password.clone();
                        Task::future(async move {
                            let jid: Result<JID, _> = jid_str.parse();
                            match jid {
                                Ok(j) => {
                                    let result =
                                        LuzHandle::new(j.clone(), password.to_string(), "macaw.db")
                                            .await;
                                    match result {
                                        Ok((luz_handle, receiver)) => {
                                            let stream = ReceiverStream::new(receiver);
                                            let stream =
                                                stream.map(|message| Message::Luz(message));
                                            vec![
                                                Task::done(Message::ClientCreated(Client {
                                                    client: luz_handle,
                                                    jid: j,
                                                    connection_status: Presence::Offline(
                                                        Offline::default(),
                                                    ),
                                                })),
                                                Task::stream(stream),
                                            ]
                                        }
                                        Err(e) => {
                                            tracing::error!("error (database probably)");
                                            return vec![Task::done(Message::LoginModal(
                                                LoginModalMessage::Error(Error::DatabaseConnection),
                                            ))];
                                        }
                                    }
                                }
                                Err(_) => {
                                    tracing::error!("parsing jid");
                                    return vec![Task::done(Message::LoginModal(
                                        LoginModalMessage::Error(Error::InvalidJID(
                                            jid_str.to_string(),
                                        )),
                                    ))];
                                }
                            }
                        })
                        .then(|tasks| Task::batch(tasks))
                    }
                },
                LoginModalMessage::Error(e) => match &mut self.client {
                    Account::LoggedIn(_client) => Task::none(),
                    Account::LoggedOut {
                        jid,
                        password,
                        error,
                    } => {
                        tracing::error!("luz::new: {:?}", e);
                        *error = Some(e);
                        Task::none()
                    }
                },
            },
            Message::GotChats(chats) => {
                let mut tasks = Vec::new();
                let client = match &self.client {
                    Account::LoggedIn(client) => client,
                    Account::LoggedOut {
                        jid,
                        password,
                        error,
                    } => panic!("no client"),
                };
                for chat in chats {
                    let client = client.clone();
                    let correspondent = chat.correspondent.clone();
                    tasks.push(Task::perform(
                        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, message_history) => {
                self.chats
                    .insert(chat.correspondent.clone(), (chat, message_history));
                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 {
                        jid,
                        password,
                        error,
                    } => todo!(),
                };
                Task::future(
                    async move { client.send_message(jid, luz::chat::Body { body }).await },
                )
                .discard()
            }
        }
    }

    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 {
                    jid: _,
                    password: _,
                    error,
                } => "disconnected",
            };
            let client_jid: Cow<'_, str> = match &self.client {
                Account::LoggedIn(client) => (&client.jid).into(),
                Account::LoggedOut {
                    jid: _,
                    password: _,
                    error,
                } => 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 {
                let (chat, messages) = self.chats.get(&open_chat.jid).unwrap();
                let mut messages_view = column![];
                for (_id, message) in messages {
                    let from: Cow<'_, str> = (&message.from).into();
                    let message: Column<Message> =
                        column![text(from).size(12), text(&message.body.body)].into();
                    messages_view = messages_view.push(message);
                }
                let message_send_input = row![
                    text_input("new message", &open_chat.new_message)
                        .on_input(Message::MessageCompose),
                    button("send").on_press(Message::SendMessage(
                        chat.correspondent.clone(),
                        open_chat.new_message.clone()
                    ))
                ];
                message_view = column![
                    scrollable(messages_view)
                        .height(Fill)
                        .width(Fill)
                        .anchor_bottom(),
                    message_send_input
                ];
            } else {
                message_view = column![];
            }

            row![sidebar, 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 {
            ui = modal(ui, text("new chat"));
        }
        // 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 {
                jid,
                password,
                error,
            } => {
                let signup = container(
                    column![
                        text("Log In").size(24),
                        column![
                            column![
                                text("JID").size(12),
                                text_input("berry@macaw.chat", &jid)
                                    .on_input(|j| Message::LoginModal(LoginModalMessage::JID(j)))
                                    .on_submit(Message::LoginModal(LoginModalMessage::Submit))
                                    .padding(5),
                            ]
                            .spacing(5),
                            column![
                                text("Password").size(12),
                                text_input("", &password)
                                    .on_input(|p| Message::LoginModal(LoginModalMessage::Password(
                                        p
                                    )))
                                    .on_submit(Message::LoginModal(LoginModalMessage::Submit))
                                    .secure(true)
                                    .padding(5),
                            ]
                            .spacing(5),
                            button(text("Submit"))
                                .on_press(Message::LoginModal(LoginModalMessage::Submit)),
                        ]
                        .spacing(10)
                    ]
                    .spacing(20),
                )
                .width(300)
                .padding(10)
                .style(container::rounded_box);

                // signup.into()
                modal(ui, signup)
            }
        }
    }

    fn theme(&self) -> Theme {
        Theme::Dark
    }
}

fn modal<'a, Message>(
    base: impl Into<Element<'a, Message>>,
    content: impl Into<Element<'a, Message>>,
    // on_blur: Message,
) -> Element<'a, Message>
where
    Message: Clone + 'a,
{
    stack![
        base.into(),
        opaque(
            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)
        )
    ]
    .into()
}