use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::ops::{Deref, DerefMut}; use std::sync::Arc; use iced::futures::{SinkExt, Stream, StreamExt}; 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::{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::presence::{Offline, Presence}; use luz::CommandMessage; use luz::{roster::Contact, user::User, LuzHandle, UpdateMessage}; 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; #[derive(Serialize, Deserialize)] pub struct Config { auto_connect: bool, } impl Default for Config { fn default() -> Self { Self { auto_connect: true } } } pub struct Macaw { client: Account, config: Config, roster: HashMap, users: HashMap, presences: HashMap, chats: IndexMap)>, subscription_requests: HashSet, open_chat: Option, new_chat: Option, } pub struct OpenChat { jid: JID, new_message: String, } pub struct NewChat; impl Macaw { pub fn new(client: Option, 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 } } #[tokio::main] async fn main() -> iced::Result { tracing_subscriber::fmt::init(); let cfg: Config = confy::load("macaw", None).unwrap(); let entry = Entry::new("macaw", "macaw"); let mut client_creation_error: Option = None; let mut creds: Option = 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)> = None; if let Some(creds) = creds { let jid = creds.jid.parse::(); match jid { Ok(jid) => { let luz = LuzHandle::new(jid.clone(), creds.password.to_string(), "macaw.db").await; match luz { Ok((handle, recv)) => client = Some((jid.as_bare(), handle, recv)), Err(e) => client_creation_error = Some(Error::ClientCreation(e)), } } 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 = { if cfg.auto_connect { Task::batch([Task::stream(stream), Task::done(Message::Connect)]) } else { Task::stream(stream) } }; iced::application("Macaw", Macaw::update, Macaw::view).run_with(|| { ( Macaw::new( Some(Client { client: luz_handle, // TODO: jid, connection_status: Presence::Offline(Offline::default()), }), cfg, ), // TODO: autoconnect config 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), Connect, Disconnect, OpenChat(JID), GotChats(Vec), GotMessageHistory(Chat, IndexMap), CloseChat(JID), MessageCompose(String), SendMessage(JID, String), Error(Error), } #[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), } #[derive(Debug, Error, Clone)] pub enum CredentialsSaveError { #[error("keyring: {0}")] Keyring(Arc), #[error("toml serialisation: {0}")] Toml(#[from] toml::ser::Error), } impl From 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), #[error("toml serialisation: {0}")] Toml(#[from] toml::de::Error), #[error("invalid jid: {0}")] JID(#[from] jid::ParseError), } impl From for CredentialsLoadError { fn from(e: keyring::Error) -> Self { Self::Keyring(Arc::new(e)) } } 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(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, 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(); 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)> = chats // .into_iter() // .map(|chat| (chat.correspondent.clone(), (chat, IndexMap::new()))) // .collect(); Message::GotChats(chats) }), 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)> = 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(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(OpenChat { jid, new_message: String::new(), }); 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::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( async move { (chat, client.get_messages(correspondent).await) }, |result| { let messages: IndexMap = 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(_) => { 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) => todo!("error notification toasts, logging?"), } } fn view(&self) -> Element { let mut ui: Element = { let mut chats_list: Column = column![]; for (jid, chat) in &self.chats { let cow_jid: Cow<'_, str> = (jid).into(); let mut toggler: Toggler = 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 { 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 = 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> = 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 { Theme::Dark } } fn modal<'a, Message>( base: impl Into>, content: impl Into>, on_blur: Option, ) -> 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() }