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



                                                                                              



                                                                 
                                 
                                           
                  
 
                  
                    



                                                  































































                                                  



                                    



















                                                                               


                       
                                  





                                  





                        
 
            





                                                                  







                                                                              
                     




                                        
                                                    










                                                                                  




































                                                                                     

                                                                

                                                      
                                                                                  














                                                                                   




























                                                                                 
                                              
























































































                                                                                                    


                                        
















































                                                                                    
          















































                                                                                                  




                              


























                                                               
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::text::{Fragment, IntoFragment};
use iced::widget::{
    button, center, column, container, mouse_area, opaque, row, stack, text, text_input, Text,
};
use iced::{stream, Color, Element, Subscription, Task, Theme};
use jid::JID;
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;

pub struct Macaw {
    client: Account,
    roster: HashMap<JID, Contact>,
    users: HashMap<JID, User>,
    presences: HashMap<JID, Presence>,
    chats: HashMap<JID, (Chat, Vec<ChatMessage>)>,
    subscription_requests: HashSet<JID>,
}

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: HashMap::new(),
            subscription_requests: HashSet::new(),
        }
    }
}

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

#[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.push(message);
                    } else {
                        let chat = Chat {
                            correspondent: to.clone(),
                        };
                        let message_history = vec![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 (send, recv) = oneshot::channel();
                Task::perform(
                    async move {
                        client.client.send(CommandMessage::GetRoster(send)).await;
                        recv.await
                    },
                    |result| {
                        let roster = result.unwrap().unwrap();
                        let mut macaw_roster = HashMap::new();
                        for contact in roster {
                            macaw_roster.insert(contact.user_jid.clone(), contact);
                        }
                        Message::Roster(macaw_roster)
                    },
                )
            }
            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) => todo!(),
            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()
                    }
                },
            },
        }
    }

    fn view(&self) -> Element<Message> {
        let ui = {
            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(),
                );
            }
            let column = column(contacts);
            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",
            };
            // match &self.client.as_ref().map(|client| &client.connection_status) {
            //     Some(s) => match s {
            //         Presence::Online(online) => "connected",
            //         Presence::Offline(offline) => "disconnected",
            //     },
            //     None => "no account",
            // };
            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());
            };
            column![
                row![
                    text(client_jid),
                    text(connection_status),
                    button("connect").on_press(Message::Connect),
                    button("disconnect").on_press(Message::Disconnect)
                ],
                text("Buddy List:"),
                //
                //
                column,
            ]
        };

        // temporarily center to fill space
        let ui = center(ui).into();

        match &self.client {
            Account::LoggedIn(_client) => ui,
            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()
}