diff options
Diffstat (limited to 'src/main.rs')
-rw-r--r-- | src/main.rs | 883 |
1 files changed, 702 insertions, 181 deletions
diff --git a/src/main.rs b/src/main.rs index 1a979c3..8b22ce3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,17 @@ -use std::borrow::Cow; +use std::borrow::{Borrow, Cow}; +use std::cell::{Ref, RefCell}; use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::ops::{Deref, DerefMut}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; +use std::rc::{Rc, Weak}; use std::str::FromStr; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use chrono::{Local, Utc}; use filamento::chat::{Chat, Message as ChatMessage}; -use filamento::error::CommandError; +use filamento::error::{CommandError, DatabaseError}; +use filamento::files::Files; use filamento::presence::{Offline, Presence, PresenceType}; use filamento::{roster::Contact, user::User, UpdateMessage}; use iced::alignment::Horizontal::Right; @@ -21,7 +24,7 @@ use iced::theme::{Custom, Palette}; use iced::widget::button::Status; use iced::widget::text::{Fragment, IntoFragment, Wrapping}; use iced::widget::{ - button, center, checkbox, column, container, horizontal_space, mouse_area, opaque, row, + button, center, checkbox, column, container, horizontal_space, image, mouse_area, opaque, row, scrollable, stack, text, text_input, toggler, Column, Text, Toggler, }; use iced::Length::{self, Fill, Shrink}; @@ -33,6 +36,7 @@ use login_modal::{Creds, LoginModal}; use message_view::MessageView; use serde::{Deserialize, Serialize}; use thiserror::Error; +use tokio::sync::mpsc::Sender; use tokio::sync::{mpsc, oneshot}; use tokio_stream::wrappers::ReceiverStream; use tracing::{error, info}; @@ -60,18 +64,156 @@ impl Default for Config { } } +// any object that references another contains an arc to that object, so that items can be garbage-collected by checking reference count +// maybe have a cache which is a set of an enum of reference counted objects, so that when an object is needed it's first cloned from the set, otherwise it is added then cloned. then once an object is no longer needed, it is automatically garbage collected. +// or maybe have the cache items automatically drop themselves at 1 reference? some kind of custom pointer. items in the cache must be easily addressable and updateable. pub struct Macaw { client: Account, config: Config, - roster: HashMap<JID, Contact>, - users: HashMap<JID, User>, - presences: HashMap<JID, Presence>, - chats: IndexMap<JID, (Chat, Option<ChatMessage>)>, + // references users + messages: HashMap<Uuid, Weak<RefCell<MacawMessage>>>, + // references users + roster: HashMap<JID, MacawContact>, + // store count of how many things reference it. allows it to stay mutable. + // or maybe store a bool that indicates whether it can be garbage collected + // but then in that case, if you change the bool, then it can be dropped anyway.... + // realistically none of this stuff matters until there are group chats. and group chats will have a list of users anyway. + // so whenever a group chat is closed any users that are both not in the roster and that one doesn't also have a chat with + // can be dropped. + // but then also users who are no longer in the chat but were loaded because of old messages must also be dropped. + // so the set of users in the group chat must also include people who left, just marked as do-not-show/departed. solution! + // this only doesn't work if there are multiple group chats open at the same time ig. in this case the other chats' user + // lists would need to also be differenced. + // i'm pretty sure this is just O(2 + n) where n = number of other group chats open for each drop attempt, and it can + // happen in a separate thread in the background anyway so no slowdown. + // TODO: add presences reference + // references nothing, optionally contact + users: HashMap<JID, Weak<RefCell<MacawUser>>>, + // chat could have no messages, and therefore no latest message. + // references users, latest message + chats: IndexMap<JID, Rc<RefCell<MacawChat>>>, subscription_requests: HashSet<JID>, open_chat: Option<MessageView>, new_chat: Option<NewChat>, } +#[derive(Debug)] +pub struct MacawUser { + inner: User, + contact: Option<Rc<RefCell<MacawContact>>>, +} + +impl Deref for MacawUser { + type Target = User; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for MacawUser { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl MacawUser { + pub fn contact(&self) -> Option<Ref<'_, MacawContact>> { + self.contact + .as_ref() + .map(|contact| contact.as_ref().borrow()) + } +} + +#[derive(Debug, Clone)] +pub struct MacawMessage { + inner: ChatMessage, + user: Rc<RefCell<MacawUser>>, +} + +impl Deref for MacawMessage { + type Target = ChatMessage; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for MacawMessage { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl MacawMessage { + pub fn user(&self) -> Ref<'_, MacawUser> { + let user = self.user.as_ref().borrow(); + user + } +} + +#[derive(Debug)] +pub struct MacawContact { + inner: Contact, + user: Rc<RefCell<MacawUser>>, +} + +impl Deref for MacawContact { + type Target = Contact; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for MacawContact { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl MacawContact { + pub fn user(&self) -> Ref<'_, MacawUser> { + let user = self.user.as_ref().borrow(); + user + } +} + +pub struct MacawChat { + inner: Chat, + user: Rc<RefCell<MacawUser>>, + message: Option<Rc<RefCell<MacawMessage>>>, +} + +impl Deref for MacawChat { + type Target = Chat; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for MacawChat { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl MacawChat { + pub fn user(&self) -> Ref<'_, MacawUser> { + let user = self.user.as_ref().borrow(); + user + } + + pub fn latest_message(&self) -> Option<Ref<'_, MacawMessage>> { + let latest_message = self + .message + .as_ref() + .map(|message| message.as_ref().borrow()); + latest_message + } +} + pub struct NewChat; impl Macaw { @@ -88,11 +230,11 @@ impl Macaw { config, roster: HashMap::new(), users: HashMap::new(), - presences: HashMap::new(), chats: IndexMap::new(), subscription_requests: HashSet::new(), open_chat: None, new_chat: None, + messages: HashMap::new(), } } } @@ -124,7 +266,8 @@ impl Account { #[derive(Clone, Debug)] pub struct Client { - client: filamento::Client, + client: filamento::Client<Files>, + files_root: PathBuf, jid: JID, status: Presence, connection_state: ConnectionState, @@ -134,6 +277,10 @@ impl Client { pub fn is_connected(&self) -> bool { self.connection_state.is_connected() } + + pub fn files_root(&self) -> &Path { + &self.files_root + } } #[derive(Clone, Debug)] @@ -160,7 +307,7 @@ impl DerefMut for Client { } impl Deref for Client { - type Target = filamento::Client; + type Target = filamento::Client<Files>; fn deref(&self) -> &Self::Target { &self.client @@ -171,7 +318,10 @@ async fn filamento( jid: &JID, creds: &Creds, cfg: &Config, -) -> (filamento::Client, mpsc::Receiver<UpdateMessage>) { +) -> ( + (filamento::Client<Files>, mpsc::Receiver<UpdateMessage>), + PathBuf, +) { let filamento; if let Some(ref dburl) = cfg.dburl { // TODO: have some sort of crash popup for this stuff @@ -180,29 +330,94 @@ async fn filamento( let db = filamento::db::Db::create_connect_and_migrate(db_path) .await .unwrap(); - filamento = filamento::Client::new(jid.clone(), creds.password.to_string(), db); - } else if let Some(ref dir) = cfg.storage_dir { - let mut data_dir = PathBuf::from_str(&dir).expect("invalid storage directory path"); - data_dir.push(creds.jid.clone()); - data_dir.push(creds.jid.clone()); - data_dir.set_extension("db"); - let db = filamento::db::Db::create_connect_and_migrate(data_dir) - .await - .unwrap(); - filamento = filamento::Client::new(jid.clone(), creds.password.to_string(), db); + let files; + if let Some(ref dir) = cfg.storage_dir { + let mut data_dir = PathBuf::from_str(&dir).expect("invalid storage directory path"); + data_dir.push(creds.jid.clone()); + let files_dir = data_dir.join("files"); + files = Files::new(&files_dir); + if !tokio::fs::try_exists(&files_dir) + .await + .expect("could not read storage directory") + { + tokio::fs::create_dir_all(&files_dir) + .await + .expect("could not create file storage directory") + } + filamento = ( + filamento::Client::new(jid.clone(), creds.password.to_string(), db, files), + files_dir, + ); + } else { + let mut data_dir = dirs::data_dir().expect( + "operating system does not support retreiving determining default data dir", + ); + data_dir.push("macaw"); + data_dir.push(creds.jid.clone()); + data_dir.push("files"); + let files_dir = data_dir; + files = Files::new(&files_dir); + if !tokio::fs::try_exists(&files_dir) + .await + .expect("could not read storage directory") + { + tokio::fs::create_dir_all(&files_dir) + .await + .expect("could not create file storage directory") + } + filamento = ( + filamento::Client::new(jid.clone(), creds.password.to_string(), db, files), + files_dir, + ); + } } else { - let mut data_dir = dirs::data_dir() - .expect("operating system does not support retreiving determining default data dir"); - data_dir.push("macaw"); - data_dir.push(creds.jid.clone()); - data_dir.push(creds.jid.clone()); - // TODO: better lol - data_dir.set_extension("db"); - info!("db_path: {:?}", data_dir); - let db = filamento::db::Db::create_connect_and_migrate(data_dir) - .await - .unwrap(); - filamento = filamento::Client::new(jid.clone(), creds.password.to_string(), db); + if let Some(ref dir) = cfg.storage_dir { + let mut data_dir = PathBuf::from_str(&dir).expect("invalid storage directory path"); + data_dir.push(creds.jid.clone()); + let files_dir = data_dir.join("files"); + let files = Files::new(&files_dir); + data_dir.push(format!("{}.db", creds.jid.clone())); + let db = filamento::db::Db::create_connect_and_migrate(data_dir) + .await + .unwrap(); + if !tokio::fs::try_exists(&files_dir) + .await + .expect("could not read storage directory") + { + tokio::fs::create_dir_all(&files_dir) + .await + .expect("could not create file storage directory") + } + filamento = ( + filamento::Client::new(jid.clone(), creds.password.to_string(), db, files), + files_dir, + ); + } else { + let mut data_dir = dirs::data_dir().expect( + "operating system does not support retreiving determining default data dir", + ); + data_dir.push("macaw"); + data_dir.push(creds.jid.clone()); + let files_dir = data_dir.join("files"); + let files = Files::new(&files_dir); + data_dir.push(format!("{}.db", creds.jid.clone())); + info!("db_path: {:?}", data_dir); + let db = filamento::db::Db::create_connect_and_migrate(data_dir) + .await + .unwrap(); + if !tokio::fs::try_exists(&files_dir) + .await + .expect("could not read storage directory") + { + tokio::fs::create_dir_all(&files_dir) + .await + .expect("could not create file storage directory") + } + filamento = ( + filamento::Client::new(jid.clone(), creds.password.to_string(), db, files), + files_dir, + ); + } } filamento } @@ -247,19 +462,24 @@ async fn main() -> iced::Result { } } - let mut client: Option<(JID, filamento::Client, mpsc::Receiver<UpdateMessage>)> = None; + let mut client: Option<( + JID, + filamento::Client<Files>, + mpsc::Receiver<UpdateMessage>, + PathBuf, + )> = None; if let Some(creds) = creds { let jid = creds.jid.parse::<JID>(); match jid { Ok(jid) => { - let (handle, updates) = filamento(&jid, &creds, &cfg).await; - client = Some((jid, handle, updates)); + let ((handle, updates), files_dir) = filamento(&jid, &creds, &cfg).await; + client = Some((jid, handle, updates, files_dir)); } Err(e) => client_creation_error = Some(Error::CredentialsLoad(e.into())), } } - if let Some((jid, luz_handle, update_recv)) = client { + if let Some((jid, luz_handle, update_recv, files_root)) = client { let stream = ReceiverStream::new(update_recv); let stream = stream.map(|message| Message::Luz(message)); let task = { @@ -270,14 +490,14 @@ async fn main() -> iced::Result { [ Task::batch([ Task::perform( - async move { luz_handle1.get_roster().await }, + async move { luz_handle1.get_roster_with_users().await }, |result| { let roster = result.unwrap(); let mut macaw_roster = HashMap::new(); for contact in roster { - macaw_roster.insert(contact.user_jid.clone(), contact); + macaw_roster.insert(contact.0.user_jid.clone(), contact); } - Message::Roster(macaw_roster) + Message::RosterWithUsers(macaw_roster) }, ), Task::perform( @@ -297,14 +517,17 @@ async fn main() -> iced::Result { ) } else { Task::batch([ - Task::perform(async move { luz_handle1.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 { luz_handle1.get_roster_with_users().await }, + |result| { + let roster = result.unwrap(); + let mut macaw_roster = HashMap::new(); + for contact in roster { + macaw_roster.insert(contact.0.user_jid.clone(), contact); + } + Message::RosterWithUsers(macaw_roster) + }, + ), Task::perform( async move { luz_handle2.get_chats_ordered_with_latest_messages().await }, |chats| { @@ -325,14 +548,13 @@ async fn main() -> iced::Result { Macaw::new( Some(Client { client: luz_handle, - // TODO: jid, - // TODO: store cached status status: Presence { timestamp: Utc::now(), presence: PresenceType::Offline(Offline::default()), }, connection_state: ConnectionState::Offline, + files_root, }), cfg, ), @@ -383,11 +605,10 @@ pub enum Message { LoginModal(login_modal::Message), ClientCreated(Client), Luz(UpdateMessage), - Roster(HashMap<JID, Contact>), + RosterWithUsers(HashMap<JID, (Contact, User)>), Connect, Disconnect, GotChats(Vec<(Chat, ChatMessage)>), - GotMessageHistory(Chat, IndexMap<Uuid, ChatMessage>), ToggleChat(JID), SendMessage(JID, String), Error(Error), @@ -440,10 +661,6 @@ 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.status = Presence { @@ -452,11 +669,57 @@ impl Macaw { }; client.connection_state = ConnectionState::Online; let mut roster = HashMap::new(); + let mut get_users = Vec::new(); for contact in vec { - roster.insert(contact.user_jid.clone(), contact); + if let Some(Some(user)) = + self.users.get(&contact.user_jid).map(|user| user.upgrade()) + { + let contact = MacawContact { + inner: contact, + user, + }; + roster.insert(contact.user_jid.clone(), contact); + user.borrow_mut().contact = Some(Rc::new(RefCell::new(contact))) + } else { + match self.client { + Account::LoggedIn(client) => get_users.push(Task::perform( + client.get_user(contact.user_jid), + |result| { + let result = result.unwrap(); + (contact, result) + }, + )), + Account::LoggedOut(login_modal) => {} + } + } + } + if get_users.is_empty() { + self.roster = roster; + Task::none() + } else { + // TODO: potential race condition if two rosters are gotten at the same time? + Task::batch(get_users).collect().then(|users| { + for (contact, user) in users { + let user = Rc::new(RefCell::new(MacawUser { + inner: user, + contact: None, + })); + let contact = MacawContact { + inner: contact, + user, + }; + roster.insert(contact.user_jid, contact); + user.borrow_mut().contact = + Some(Rc::new(RefCell::new(contact))); + self.users.insert( + contact.user_jid, + Rc::<RefCell<MacawUser>>::downgrade(&user), + ); + } + self.roster = roster; + Task::none() + }) } - self.roster = roster; - Task::none() } Account::LoggedOut(login_modal) => Task::none(), }, @@ -475,49 +738,225 @@ impl Macaw { } } UpdateMessage::FullRoster(vec) => { - let mut macaw_roster = HashMap::new(); + let mut roster = HashMap::new(); + let mut get_users = Vec::new(); for contact in vec { - macaw_roster.insert(contact.user_jid.clone(), contact); + if let Some(Some(user)) = + self.users.get(&contact.user_jid).map(|user| user.upgrade()) + { + let contact = MacawContact { + inner: contact, + user, + }; + roster.insert(contact.user_jid.clone(), contact); + user.borrow_mut().contact = Some(Rc::new(RefCell::new(contact))) + } else { + match self.client { + Account::LoggedIn(client) => get_users.push(Task::perform( + client.get_user(contact.user_jid), + |result| { + let result = result.unwrap(); + (contact, result) + }, + )), + Account::LoggedOut(login_modal) => {} + } + } + } + if get_users.is_empty() { + self.roster = roster; + Task::none() + } else { + // TODO: potential race condition if two rosters are gotten at the same time? + Task::batch(get_users).collect().then(|users| { + for (contact, user) in users { + let user = Rc::new(RefCell::new(MacawUser { + inner: user, + contact: None, + })); + let contact = MacawContact { + inner: contact, + user, + }; + roster.insert(contact.user_jid, contact); + user.borrow_mut().contact = Some(Rc::new(RefCell::new(contact))); + self.users.insert( + contact.user_jid, + Rc::<RefCell<MacawUser>>::downgrade(&user), + ); + } + self.roster = roster; + Task::none() + }) } - self.roster = macaw_roster; - Task::none() } UpdateMessage::RosterUpdate(contact) => { - self.roster.insert(contact.user_jid.clone(), contact); - Task::none() + if let Some(Some(user)) = + self.users.get(&contact.user_jid).map(|user| user.upgrade()) + { + let contact = MacawContact { + inner: contact, + user, + }; + self.roster.insert(contact.user_jid.clone(), contact); + user.borrow_mut().contact = Some(Rc::new(RefCell::new(contact))); + Task::none() + } else { + match self.client { + Account::LoggedIn(client) => { + Task::perform(client.get_user(contact.user_jid), |result| { + let result = result.unwrap(); + (contact, result) + }) + .then(|(contact, user)| { + let user = Rc::new(RefCell::new(MacawUser { + inner: user, + contact: None, + })); + let contact = MacawContact { + inner: contact, + user, + }; + self.roster.insert(contact.user_jid.clone(), contact); + user.borrow_mut().contact = + Some(Rc::new(RefCell::new(contact))); + self.users.insert( + contact.user_jid, + Rc::<RefCell<MacawUser>>::downgrade(&user), + ); + Task::none() + }) + } + Account::LoggedOut(login_modal) => Task::none(), + } + } } UpdateMessage::RosterDelete(jid) => { self.roster.remove(&jid); Task::none() } UpdateMessage::Presence { from, presence } => { - self.presences.insert(from, presence); + // TODO: presence handling Task::none() } UpdateMessage::Message { to, message } => { - if let Some((chat_jid, (chat, old_message))) = - self.chats.shift_remove_entry(&to) + if let Some(Some(user)) = + self.users.get(&message.from).map(|user| user.upgrade()) { - self.chats - .insert_before(0, chat_jid, (chat, Some(message.clone()))); - if let Some(open_chat) = &mut self.open_chat { - if open_chat.jid == to { - open_chat.update(message_view::Message::Message(message)); + let message = MacawMessage { + inner: message, + user, + }; + let message = Rc::new(RefCell::new(message)); + self.messages.insert( + message.as_ref().borrow().id, + Rc::<RefCell<MacawMessage>>::downgrade(&message), + ); + if let Some((chat_jid, chat)) = self.chats.shift_remove_entry(&to) { + chat.as_ref().borrow_mut().message = Some(message); + self.chats.insert_before(0, chat_jid, chat); + if let Some(open_chat) = &mut self.open_chat { + if open_chat.chat().user().jid == to { + open_chat.messages.push(message); + } } + } else { + let chat = Chat { + correspondent: to.clone(), + // TODO: should have a new chat event first... + have_chatted: false, + }; + let chat = MacawChat { + inner: chat, + user, + message: Some(message), + }; + self.chats.insert_before(0, to, Rc::new(RefCell::new(chat))); } + Task::none() } else { - let chat = Chat { - correspondent: to.clone(), - }; - let message_history = indexmap! {message.id => message.clone()}; - self.chats.insert_before(0, to, (chat, Some(message))); + match self.client { + Account::LoggedIn(client) => { + Task::perform(client.get_user(message.from), |result| { + let result = result.unwrap(); + result + }) + .then(|user| { + let user = Rc::new(RefCell::new(MacawUser { + inner: user, + contact: None, + })); + self.users.insert( + user.as_ref().borrow().jid, + Rc::<RefCell<MacawUser>>::downgrade(&user), + ); + let message = MacawMessage { + inner: message, + user, + }; + let message = Rc::new(RefCell::new(message)); + self.messages.insert( + message.as_ref().borrow().id, + Rc::<RefCell<MacawMessage>>::downgrade(&message), + ); + if let Some((chat_jid, chat)) = + self.chats.shift_remove_entry(&to) + { + chat.as_ref().borrow_mut().message = Some(message); + self.chats.insert_before(0, chat_jid, chat); + if let Some(open_chat) = &mut self.open_chat { + if open_chat.chat().user().jid == to { + open_chat.messages.push(message); + } + } + } else { + let chat = Chat { + correspondent: to.clone(), + // TODO: should have a new chat event first... + have_chatted: false, + }; + let chat = MacawChat { + inner: chat, + user, + message: Some(message), + }; + self.chats.insert_before( + 0, + to, + Rc::new(RefCell::new(chat)), + ); + } + Task::none() + }) + } + Account::LoggedOut(login_modal) => Task::none(), + } } - Task::none() } UpdateMessage::SubscriptionRequest(jid) => { // TODO: subscription requests Task::none() } + UpdateMessage::MessageDelivery { id, delivery } => { + if let Some(Some(message)) = + self.messages.get(&id).map(|message| message.upgrade()) + { + message.as_ref().borrow_mut().delivery = Some(delivery) + } + Task::none() + } + UpdateMessage::NickChanged { jid, nick } => { + if let Some(Some(user)) = self.users.get(&jid).map(|user| user.upgrade()) { + user.as_ref().borrow_mut().nick = nick + } + Task::none() + } + UpdateMessage::AvatarChanged { jid, id } => { + if let Some(Some(user)) = self.users.get(&jid).map(|user| user.upgrade()) { + user.as_ref().borrow_mut().avatar = id + } + Task::none() + } }, // TODO: NEXT Message::ClientCreated(client) => { @@ -526,14 +965,17 @@ impl Macaw { 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 { client1.client.get_roster_with_users().await }, + |result| { + let roster = result.unwrap(); + let mut macaw_roster = HashMap::new(); + for contact in roster { + macaw_roster.insert(contact.0.user_jid.clone(), contact); + } + Message::RosterWithUsers(macaw_roster) + }, + ), Task::perform( async move { client2 @@ -555,14 +997,17 @@ impl Macaw { .chain(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 { client1.client.get_roster_with_users().await }, + |result| { + let roster = result.unwrap(); + let mut macaw_roster = HashMap::new(); + for contact in roster { + macaw_roster.insert(contact.0.user_jid.clone(), contact); + } + Message::RosterWithUsers(macaw_roster) + }, + ), Task::perform( async move { client2 @@ -583,8 +1028,22 @@ impl Macaw { ]) } } - Message::Roster(hash_map) => { - self.roster = hash_map; + Message::RosterWithUsers(hash_map) => { + for (_, (contact, user)) in hash_map { + let user = MacawUser { + inner: user, + contact: None, + }; + let user = Rc::new(RefCell::new(user)); + let contact = MacawContact { + inner: contact, + user, + }; + self.roster.insert(contact.user_jid, contact); + user.borrow_mut().contact = Some(Rc::new(RefCell::new(contact))); + self.users + .insert(contact.user_jid, Rc::<RefCell<MacawUser>>::downgrade(&user)); + } Task::none() } Message::Connect => match &mut self.client { @@ -611,29 +1070,52 @@ impl Macaw { Message::ToggleChat(jid) => { match &self.open_chat { Some(message_view) => { - if message_view.jid == jid { + if message_view.chat().user().jid == jid { self.open_chat = None; return Task::none(); } } None => {} } - self.open_chat = Some(MessageView::new(jid.clone(), &self.config)); - let jid1 = jid.clone(); - match &self.client { - Account::LoggedIn(client) => { - let client = client.clone(); - Task::perform( - async move { client.get_messages(jid1).await }, - move |result| match result { - Ok(h) => { - Message::MessageView(message_view::Message::MessageHistory(h)) - } - Err(e) => Message::Error(Error::MessageHistory(jid.clone(), e)), - }, - ) + if let Some(chat) = self.chats.get(&jid) { + match &self.client { + Account::LoggedIn(client) => { + let client = client.clone(); + Task::perform( + async move { client.get_messages(jid).await }, + move |result| { + let message_history = result.unwrap(); + let messages = Vec::new(); + for message in message_history { + // TODO: there must be users for the messages, but won't work for group chats. + let user = self + .users + .get(&message.from) + .unwrap() + .upgrade() + .unwrap(); + let message = MacawMessage { + inner: message, + user, + }; + let message = Rc::new(RefCell::new(message)); + self.messages.insert( + message.as_ref().borrow().id, + Rc::<RefCell<MacawMessage>>::downgrade(&message), + ); + messages.push(message) + } + let open_chat = MessageView::new(chat.clone(), &self.config); + open_chat.messages = messages; + self.open_chat = Some(open_chat); + }, + ) + .discard() + } + Account::LoggedOut(login_modal) => Task::none(), } - Account::LoggedOut(login_modal) => Task::none(), + } else { + Task::none() } } Message::LoginModal(login_modal_message) => match &mut self.client { @@ -650,9 +1132,9 @@ impl Macaw { Ok(jid) => { Task::perform(async move { let (jid, creds, config) = (jid, creds, config); - let (handle, recv) = filamento(&jid, &creds, &config).await; - (handle, recv, jid, creds, config) - }, move |(handle, recv, jid, creds, config)| { + let ((handle, recv), files_root) = filamento(&jid, &creds, &config).await; + (handle, recv, jid, creds, config, files_root) + }, move |(handle, recv, jid, creds, config, files_root)| { let creds = creds; let mut tasks = Vec::new(); tasks.push(Task::done(crate::Message::ClientCreated( @@ -661,6 +1143,7 @@ impl Macaw { jid, status: Presence { timestamp: Utc::now(), presence: PresenceType::Offline(Offline::default()) }, connection_state: ConnectionState::Offline, + files_root, }, ))); let stream = ReceiverStream::new(recv); @@ -720,45 +1203,16 @@ impl Macaw { return Task::none(); } }; - for chat in chats { + for (chat, message) in chats { + let chat = MacawChat { + inner: todo!(), + user: todo!(), + message: todo!(), + } self.chats - // TODO: could have a chat with no messages, bad database state .insert(chat.0.correspondent.clone(), (chat.0.clone(), Some(chat.1))); - // let client = client.clone(); - // let correspondent = chat.correspondent.clone(); - // tasks.push(Task::perform( - // // TODO: don't get the entire message history LOL - // 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, mut message_history) => { - // TODO: don't get the entire message history LOL - if let Some((_id, message)) = message_history.pop() { - self.chats - .insert(chat.correspondent.clone(), (chat, Some(message))); - } - Task::none() } Message::SendMessage(jid, body) => { let client = match &self.client { @@ -784,9 +1238,10 @@ impl Macaw { let action = message_view.update(message); match action { message_view::Action::None => Task::none(), - message_view::Action::SendMessage(m) => { - Task::done(Message::SendMessage(message_view.jid.clone(), m)) - } + message_view::Action::SendMessage(m) => Task::done(Message::SendMessage( + message_view.chat().user().jid.clone(), + m, + )), } } else { Task::none() @@ -812,15 +1267,18 @@ impl Macaw { fn view(&self) -> Element<Message> { let mut ui: Element<Message> = { let mut chats_list: Column<Message> = column![]; - for (jid, (chat, latest_message)) in &self.chats { - let mut open = false; - if let Some(open_chat) = &self.open_chat { - if open_chat.jid == *jid { - open = true; + if let Account::LoggedIn(client) = &self.client { + for (jid, chat) in &self.chats { + let mut open = false; + if let Some(open_chat) = &self.open_chat { + if open_chat.chat().user().jid == *jid { + open = true; + } } + let chat_list_item = + chat_list_item(client.files_root(), chat.as_ref().borrow(), open); + chats_list = chats_list.push(chat_list_item); } - let chat_list_item = chat_list_item(chat, latest_message, open); - chats_list = chats_list.push(chat_list_item); } let chats_list = scrollable(chats_list.spacing(8).padding(8)) .spacing(1) @@ -1021,34 +1479,97 @@ where stack![base.into(), opaque(mouse_area)].into() } -fn chat_list_item<'a>( - chat: &'a Chat, - latest_message: &'a Option<ChatMessage>, - open: bool, -) -> Element<'a, Message> { - let mut content: Column<Message> = column![text(chat.correspondent.to_string())]; - if let Some(latest_message) = latest_message { +fn chat_list_item<'a, C>(file_root: &'a Path, chat: C, open: bool) -> Element<'a, Message> +where + C: Deref<Target = MacawChat> + 'a, +{ + let name: String; + if let Some(Some(contact_name)) = chat.user().contact().map(|contact| contact.name.clone()) { + name = contact_name + } else if let Some(nick) = &chat.user().nick { + name = nick.clone() + } else { + name = chat.correspondent().to_string(); + } + + let avatar: Option<String>; + if let Some(user_avatar) = &chat.user().avatar { + avatar = Some(user_avatar.clone()) + } else { + avatar = None + } + + let latest_message_text: Option<(String, String)>; + if let Some(latest_message) = chat.latest_message() { let message = latest_message.body.body.replace("\n", " "); let date = latest_message.timestamp.naive_local(); let now = Local::now().naive_local(); let timeinfo; if date.date() == now.date() { // TODO: localisation/config - timeinfo = text(date.time().format("%H:%M").to_string()) + timeinfo = date.time().format("%H:%M").to_string() } else { - timeinfo = text(date.date().format("%d/%m").to_string()) + timeinfo = date.date().format("%d/%m").to_string() } - content = content.push( + latest_message_text = Some((message, timeinfo)); + // content = content.push( + // row![ + // container(text(message).wrapping(Wrapping::None)) + // .clip(true) + // .width(Fill), + // timeinfo + // ] + // .spacing(8) + // .width(Fill), + // ); + } else { + latest_message_text = None; + } + + let avatar_image = if let Some(avatar) = avatar { + let path = file_root.join(avatar); + Some(image(path).width(48).height(48)) + } else { + None + }; + let content: Element<Message> = if let Some(avatar_image) = avatar_image { + if let Some((message, time)) = latest_message_text { row![ - container(text(message).wrapping(Wrapping::None)) - .clip(true) - .width(Fill), - timeinfo + avatar_image, + column![ + text(name), + row![ + container(text(message).wrapping(Wrapping::None)) + .clip(true) + .width(Fill), + text(time) + ] + .spacing(8) + .width(Fill) + ] ] - .spacing(8) - .width(Fill), - ); - } + .into() + } else { + row![avatar_image, text(name)].into() + } + } else { + if let Some((message, time)) = latest_message_text { + column![ + text(name), + row![ + container(text(message).wrapping(Wrapping::None)) + .clip(true) + .width(Fill), + text(time) + ] + .spacing(8) + .width(Fill) + ] + .into() + } else { + text(name).into() + } + }; let mut button = button(content).on_press(Message::ToggleChat(chat.correspondent.clone())); if open { button = button.style(|theme: &Theme, status| { |