diff --git a/Cargo.lock b/Cargo.lock index a58f241..8ad1a93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -643,6 +643,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "confy" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45b1f4c00870f07dc34adcac82bb6a72cc5aabca8536ba1797e01df51d2ce9a0" +dependencies = [ + "directories", + "serde", + "thiserror 1.0.69", + "toml", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -883,6 +895,30 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" +[[package]] +name = "dbus" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" +dependencies = [ + "libc", + "libdbus-sys", + "winapi", +] + +[[package]] +name = "dbus-secret-service" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42a16374481d92aed73ae45b1f120207d8e71d24fb89f357fadbd8f946fd84b" +dependencies = [ + "dbus", + "futures-util", + "num", + "once_cell", + "rand", +] + [[package]] name = "dconf_rs" version = "0.3.0" @@ -918,13 +954,22 @@ dependencies = [ "subtle", ] +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys 0.4.1", +] + [[package]] name = "dirs" version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" dependencies = [ - "dirs-sys", + "dirs-sys 0.3.7", ] [[package]] @@ -938,6 +983,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -2078,6 +2135,7 @@ dependencies = [ "rsasl", "stanza", "take_mut", + "thiserror 2.0.11", "tokio", "tokio-native-tls", "tracing", @@ -2133,6 +2191,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keyring" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f8fe839464d4e4b37d756d7e910063696af79a7e877282cb1825e4ec5f10833" +dependencies = [ + "byteorder", + "dbus-secret-service", + "log", + "security-framework 2.11.1", + "security-framework 3.2.0", + "windows-sys 0.59.0", +] + [[package]] name = "khronos-egl" version = "6.0.0" @@ -2175,6 +2247,15 @@ version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +[[package]] +name = "libdbus-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +dependencies = [ + "pkg-config", +] + [[package]] name = "libloading" version = "0.7.4" @@ -2288,6 +2369,7 @@ dependencies = [ "peanuts", "sqlx", "stanza", + "thiserror 2.0.11", "tokio", "tokio-stream", "tokio-util", @@ -2300,14 +2382,18 @@ dependencies = [ name = "macaw" version = "0.1.0" dependencies = [ + "confy", "iced", + "indexmap", "jid", + "keyring", "luz", "secret-service", "tokio", "tokio-stream", "tracing", "tracing-subscriber", + "uuid", ] [[package]] @@ -2448,7 +2534,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -2928,6 +3014,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "orbclient" version = "0.3.48" @@ -3074,6 +3166,7 @@ dependencies = [ "futures", "futures-util", "nom", + "thiserror 2.0.11", "tokio", "tracing", ] @@ -3617,6 +3710,19 @@ dependencies = [ "security-framework-sys", ] +[[package]] +name = "security-framework" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags 2.8.0", + "core-foundation 0.10.0", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework-sys" version = "2.14.0" @@ -3676,6 +3782,15 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -4111,6 +4226,7 @@ version = "0.1.0" dependencies = [ "jid", "peanuts", + "thiserror 2.0.11", ] [[package]] @@ -4408,11 +4524,26 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -4421,6 +4552,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ "indexmap", + "serde", + "serde_spanned", "toml_datetime", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index 5c1322b..03cba3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,7 @@ tokio-stream = "0.1.17" tracing-subscriber = "0.3.19" tracing = "0.1.41" secret-service = { version = "4.0.0", features = ["rt-tokio-crypto-rust"] } +confy = "0.6.1" +keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service"] } +uuid = { version = "1.13.1", features = ["v4"] } +indexmap = "2.7.1" diff --git a/src/main.rs b/src/main.rs index 1a940fd..e3d3332 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,12 +4,17 @@ 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, stack, text, text_input, Text, + 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; @@ -17,16 +22,36 @@ 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, users: HashMap, presences: HashMap, - chats: HashMap)>, + chats: IndexMap)>, subscription_requests: HashSet, + open_chat: Option, + new_chat: Option, } +pub struct OpenChat { + jid: JID, + new_message: String, +} + +pub struct NewChat; + pub struct Creds { jid: String, password: String, @@ -50,8 +75,10 @@ impl Macaw { roster: HashMap::new(), users: HashMap::new(), presences: HashMap::new(), - chats: HashMap::new(), + chats: IndexMap::new(), subscription_requests: HashSet::new(), + open_chat: None, + new_chat: None, } } } @@ -127,6 +154,11 @@ enum Message { Connect, Disconnect, OpenChat(JID), + GotChats(Vec), + GotMessageHistory(Chat, IndexMap), + CloseChat(JID), + MessageCompose(String), + SendMessage(JID, String), } #[derive(Debug, Clone)] @@ -197,12 +229,12 @@ impl Macaw { } UpdateMessage::Message { to, message } => { if let Some((_chat, message_history)) = self.chats.get_mut(&to) { - message_history.push(message); + message_history.insert(message.id, message); } else { let chat = Chat { correspondent: to.clone(), }; - let message_history = vec![message]; + let message_history = indexmap! {message.id => message}; self.chats.insert(to, (chat, message_history)); } Task::none() @@ -215,21 +247,26 @@ impl Macaw { // 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 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)> = chats + // .into_iter() + // .map(|chat| (chat.correspondent.clone(), (chat, IndexMap::new()))) + // .collect(); + Message::GotChats(chats) + }), + ]) } Message::Roster(hash_map) => { self.roster = hash_map; @@ -265,7 +302,13 @@ impl Macaw { error, } => Task::none(), }, - Message::OpenChat(jid) => todo!(), + 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(), @@ -356,21 +399,100 @@ impl Macaw { } }, }, + 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 = 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 { - 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 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 column = column(contacts); + 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", @@ -382,13 +504,6 @@ impl Macaw { 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 { @@ -398,25 +513,69 @@ impl Macaw { } => 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, - ] - }; + 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 { + ui = modal(ui, text("new chat")); + } // temporarily center to fill space - let ui = center(ui).into(); + // let ui = center(ui).into(); + let ui = container(ui).center_x(Fill).center_y(Fill); match &self.client { - Account::LoggedIn(_client) => ui, + Account::LoggedIn(_client) => ui.into(), Account::LoggedOut { jid, password,