aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar cel 🌸 <cel@bunny.garden>2025-02-26 19:15:52 +0000
committerLibravatar cel 🌸 <cel@bunny.garden>2025-02-26 19:15:52 +0000
commit50e84d47a458420c68ae94dfaa37d901d2a8a4f1 (patch)
treec675550d986c15d6eaf6201357e249df55eedb67
parent5a2fae397cb0269cdb2b9ce5ded742a78d58cdef (diff)
downloadmacaw-50e84d47a458420c68ae94dfaa37d901d2a8a4f1.tar.gz
macaw-50e84d47a458420c68ae94dfaa37d901d2a8a4f1.tar.bz2
macaw-50e84d47a458420c68ae94dfaa37d901d2a8a4f1.zip
feat: chats and messages view, adaptive layout
-rw-r--r--Cargo.lock137
-rw-r--r--Cargo.toml4
-rw-r--r--src/main.rs255
3 files changed, 346 insertions, 50 deletions
diff --git a/Cargo.lock b/Cargo.lock
index a58f241..8ad1a93 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -644,6 +644,18 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -884,6 +896,30 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -919,12 +955,21 @@ dependencies = [
]
[[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]]
@@ -939,6 +984,18 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2078,6 +2135,7 @@ dependencies = [
"rsasl",
"stanza",
"take_mut",
+ "thiserror 2.0.11",
"tokio",
"tokio-native-tls",
"tracing",
@@ -2134,6 +2192,20 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2176,6 +2248,15 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
]
@@ -2929,6 +3015,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3074,6 +3166,7 @@ dependencies = [
"futures",
"futures-util",
"nom",
+ "thiserror 2.0.11",
"tokio",
"tracing",
]
@@ -3618,6 +3711,19 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3677,6 +3783,15 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4111,6 +4226,7 @@ version = "0.1.0"
dependencies = [
"jid",
"peanuts",
+ "thiserror 2.0.11",
]
[[package]]
@@ -4409,10 +4525,25 @@ dependencies = [
]
[[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<JID, Contact>,
users: HashMap<JID, User>,
presences: HashMap<JID, Presence>,
- chats: HashMap<JID, (Chat, Vec<ChatMessage>)>,
+ 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,
@@ -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<Chat>),
+ GotMessageHistory(Chat, IndexMap<Uuid, ChatMessage>),
+ 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<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;
@@ -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<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 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 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 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<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 = 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,