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, users: HashMap, presences: HashMap, chats: HashMap)>, subscription_requests: HashSet, } pub struct Creds { jid: String, password: String, } impl Macaw { pub fn new(client: Option) -> 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, }, } #[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)> = 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), 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 { 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_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 { let ui = { let mut contacts: Vec> = 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>, content: impl Into>, // 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() }