aboutsummaryrefslogtreecommitdiffstats
path: root/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs944
1 files changed, 682 insertions, 262 deletions
diff --git a/src/main.rs b/src/main.rs
index 37551c8..b785562 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,44 +1,48 @@
-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, DatabaseError};
+use filamento::files::Files;
+use filamento::presence::{Offline, Presence, PresenceType};
+use filamento::{roster::Contact, user::User, UpdateMessage};
use iced::alignment::Horizontal::Right;
+use iced::font::{Stretch, Weight};
use iced::futures::{SinkExt, Stream, StreamExt};
use iced::keyboard::{on_key_press, on_key_release, Key, Modifiers};
-use iced::theme::palette::{
- Background, Danger, Extended, Pair, Primary, Secondary, Success, Warning,
-};
+use iced::theme::palette::{Background, Danger, Extended, Pair, Primary, Secondary, Success};
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,
- scrollable, stack, text, text_input, toggler, Column, Text, Toggler,
+ button, center, checkbox, column, container, horizontal_space, image, mouse_area, opaque, row,
+ scrollable, stack, text, text_input, toggler, Column, Svg, Text, Toggler,
};
use iced::Length::{self, Fill, Shrink};
-use iced::{color, stream, Color, Element, Subscription, Task, Theme};
+use iced::{color, stream, Color, Element, Font, Subscription, Task, Theme};
+use icons::Icon;
use indexmap::{indexmap, IndexMap};
use jid::JID;
use keyring::Entry;
use login_modal::{Creds, LoginModal};
-use luz::chat::{Chat, Message as ChatMessage};
-use luz::error::CommandError;
-use luz::presence::{Offline, Presence, PresenceType};
-use luz::CommandMessage;
-use luz::{roster::Contact, user::User, LuzHandle, UpdateMessage};
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};
+use tracing::{debug, error, info};
use uuid::Uuid;
+mod icons;
mod login_modal;
mod message_view;
@@ -61,16 +65,123 @@ 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>)>,
- subscription_requests: HashSet<JID>,
- open_chat: Option<MessageView>,
+ subscription_requests: HashSet<MacawUser>,
new_chat: Option<NewChat>,
+ // references chats, users, messages
+ open_chat: Option<MessageView>,
+ // references users, contacts
+ roster: HashMap<JID, MacawContact>,
+ // references chats, users, messages
+ chats_list: IndexMap<JID, ChatListItem>,
+}
+
+#[derive(Debug, Clone)]
+pub struct MacawMessage {
+ inner: ChatMessage,
+ from: 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
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct MacawUser {
+ inner: User,
+ // contact not needed, as can always query the roster store to get this option.
+ // contact: Option<Contact>,
+}
+
+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
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct MacawContact {
+ inner: Contact,
+ user: User,
+}
+
+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
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct MacawChat {
+ inner: Chat,
+ user: MacawUser,
+}
+
+pub struct ChatListItem {
+ // references chats
+ inner: MacawChat,
+ // references users, messages
+ latest_message: Option<MacawMessage>,
+}
+
+impl Deref for ChatListItem {
+ type Target = MacawChat;
+
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+}
+
+impl DerefMut for ChatListItem {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.inner
+ }
+}
+
+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
+ }
}
pub struct NewChat;
@@ -87,13 +198,12 @@ impl Macaw {
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,
+ open_chat: None,
+ roster: HashMap::new(),
+ chats_list: IndexMap::new(),
}
}
}
@@ -125,7 +235,8 @@ impl Account {
#[derive(Clone, Debug)]
pub struct Client {
- client: LuzHandle,
+ client: filamento::Client<Files>,
+ files_root: PathBuf,
jid: JID,
status: Presence,
connection_state: ConnectionState,
@@ -135,6 +246,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)]
@@ -161,47 +276,119 @@ impl DerefMut for Client {
}
impl Deref for Client {
- type Target = LuzHandle;
+ type Target = filamento::Client<Files>;
fn deref(&self) -> &Self::Target {
&self.client
}
}
-async fn luz(jid: &JID, creds: &Creds, cfg: &Config) -> (LuzHandle, mpsc::Receiver<UpdateMessage>) {
- let luz;
+async fn filamento(
+ jid: &JID,
+ creds: &Creds,
+ cfg: &Config,
+) -> (
+ (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
let db_path = dburl.strip_prefix("sqlite://").unwrap_or(&dburl);
let db_path = PathBuf::from_str(db_path).expect("invalid database path");
- let db = luz::db::Db::create_connect_and_migrate(db_path)
+ let db = filamento::db::Db::create_connect_and_migrate(db_path)
.await
.unwrap();
- luz = LuzHandle::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 = luz::db::Db::create_connect_and_migrate(data_dir)
- .await
- .unwrap();
- luz = LuzHandle::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 = luz::db::Db::create_connect_and_migrate(data_dir)
- .await
- .unwrap();
- luz = LuzHandle::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,
+ );
+ }
}
- luz
+ filamento
}
#[tokio::main]
@@ -244,66 +431,91 @@ async fn main() -> iced::Result {
}
}
- let mut client: Option<(JID, LuzHandle, 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) = luz(&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 = {
let luz_handle1 = luz_handle.clone();
let luz_handle2 = luz_handle.clone();
if cfg.auto_connect {
- Task::batch(
- [
- 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_handle2.get_chats_ordered_with_latest_messages().await
- },
- |chats| {
- let chats = chats.unwrap();
- info!("got chats: {:?}", chats);
- Message::GotChats(chats)
- },
- ),
- ])
- .chain(Task::done(Message::Connect)),
- Task::stream(stream),
- ],
- )
+ Task::batch([
+ Task::batch([
+ Task::perform(
+ async move { luz_handle1.get_roster_with_users().await },
+ |result| {
+ let roster = result.unwrap();
+ let mut macaw_roster = HashMap::new();
+ for (contact, user) in roster {
+ macaw_roster.insert(
+ contact.user_jid.clone(),
+ MacawContact {
+ inner: contact,
+ user,
+ },
+ );
+ }
+ Message::Roster(macaw_roster)
+ },
+ ),
+ Task::perform(
+ async move {
+ luz_handle2
+ .get_chats_ordered_with_latest_messages_and_users()
+ .await
+ },
+ |chats| {
+ let chats = chats.unwrap();
+ info!("got chats: {:?}", chats);
+ Message::GotChats(chats)
+ },
+ ),
+ ])
+ .chain(Task::done(Message::Connect)),
+ Task::stream(stream),
+ ])
} else {
+ debug!("no auto connect");
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_handle2.get_chats_ordered_with_latest_messages().await },
+ async move { luz_handle1.get_roster_with_users().await },
+ |result| {
+ let roster = result.unwrap();
+ let mut macaw_roster = HashMap::new();
+ for (contact, user) in roster {
+ macaw_roster.insert(
+ contact.user_jid.clone(),
+ MacawContact {
+ inner: contact,
+ user,
+ },
+ );
+ }
+ Message::Roster(macaw_roster)
+ },
+ ),
+ Task::perform(
+ async move {
+ luz_handle2
+ .get_chats_ordered_with_latest_messages_and_users()
+ .await
+ },
|chats| {
let chats = chats.unwrap();
info!("got chats: {:?}", chats);
@@ -314,7 +526,29 @@ async fn main() -> iced::Result {
])
}
};
+ let mut font = Font::with_name("K2D");
+ font.weight = Weight::Medium;
+ // font.stretch = Stretch::Condensed;
iced::application("Macaw", Macaw::update, Macaw::view)
+ .font(include_bytes!("../assets/fonts/Diolce-Regular.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-Italic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-Thin.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-ExtraBold.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-ExtraLightItalic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-ExtraLight.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-Light.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-Light.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-BoldItalic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-MediumItalic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-ThinItalic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-Medium.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-Bold.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-Regular.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-ExtraBoldItalic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-LightItalic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-SemiBoldItalic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-SemiBold.ttf"))
+ .default_font(font)
.subscription(subscription)
.theme(Macaw::theme)
.run_with(|| {
@@ -322,14 +556,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,
),
@@ -339,9 +572,49 @@ async fn main() -> iced::Result {
} else {
if let Some(e) = client_creation_error {
iced::application("Macaw", Macaw::update, Macaw::view)
+ .font(include_bytes!("../assets/fonts/Diolce-Regular.otf"))
+ .font(include_bytes!("../assets/fonts/K2D-Italic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-Thin.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-ExtraBold.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-ExtraLightItalic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-ExtraLight.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-Light.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-Light.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-BoldItalic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-MediumItalic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-ThinItalic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-Medium.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-Bold.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-Regular.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-ExtraBoldItalic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-LightItalic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-SemiBoldItalic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-SemiBold.ttf"))
+ .default_font(Font::with_name("K2D"))
+ .theme(Macaw::theme)
.run_with(|| (Macaw::new(None, cfg), Task::done(Message::Error(e))))
} else {
iced::application("Macaw", Macaw::update, Macaw::view)
+ .font(include_bytes!("../assets/fonts/Diolce-Regular.otf"))
+ .font(include_bytes!("../assets/fonts/K2D-Italic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-Thin.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-ExtraBold.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-ExtraLightItalic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-ExtraLight.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-Light.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-Light.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-BoldItalic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-MediumItalic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-ThinItalic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-Medium.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-Bold.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-Regular.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-ExtraBoldItalic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-LightItalic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-SemiBoldItalic.ttf"))
+ .font(include_bytes!("../assets/fonts/K2D-SemiBold.ttf"))
+ .default_font(Font::with_name("K2D"))
+ .theme(Macaw::theme)
.run_with(|| (Macaw::new(None, cfg), Task::none()))
}
}
@@ -380,11 +653,10 @@ pub enum Message {
LoginModal(login_modal::Message),
ClientCreated(Client),
Luz(UpdateMessage),
- Roster(HashMap<JID, Contact>),
+ Roster(HashMap<JID, MacawContact>),
Connect,
Disconnect,
- GotChats(Vec<(Chat, ChatMessage)>),
- GotMessageHistory(Chat, IndexMap<Uuid, ChatMessage>),
+ GotChats(Vec<((Chat, User), (ChatMessage, User))>),
ToggleChat(JID),
SendMessage(JID, String),
Error(Error),
@@ -394,13 +666,13 @@ pub enum Message {
#[derive(Debug, Error, Clone)]
pub enum Error {
#[error("failed to create Luz client: {0}")]
- ClientCreation(#[from] luz::error::DatabaseError),
+ ClientCreation(#[from] filamento::error::DatabaseError),
#[error("failed to save credentials: {0}")]
CredentialsSave(CredentialsSaveError),
#[error("failed to load credentials: {0}")]
CredentialsLoad(CredentialsLoadError),
#[error("failed to retreive messages for chat {0}")]
- MessageHistory(JID, CommandError<luz::error::DatabaseError>),
+ MessageHistory(JID, CommandError<filamento::error::DatabaseError>),
}
#[derive(Debug, Error, Clone)]
@@ -437,10 +709,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 {
@@ -448,10 +716,19 @@ impl Macaw {
presence: PresenceType::Online(online),
};
client.connection_state = ConnectionState::Online;
- let mut roster = HashMap::new();
- for contact in vec {
- roster.insert(contact.user_jid.clone(), contact);
- }
+ let roster = vec
+ .into_iter()
+ .map(|(contact, user)| {
+ (
+ contact.user_jid.clone(),
+ MacawContact {
+ inner: contact,
+ user,
+ },
+ )
+ })
+ .collect();
+ // no need to also update users as any user updates will come in separately
self.roster = roster;
Task::none()
}
@@ -471,16 +748,14 @@ impl Macaw {
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);
+ UpdateMessage::RosterUpdate(contact, user) => {
+ self.roster.insert(
+ contact.user_jid.clone(),
+ MacawContact {
+ inner: contact,
+ user,
+ },
+ );
Task::none()
}
UpdateMessage::RosterDelete(jid) => {
@@ -488,26 +763,40 @@ impl Macaw {
Task::none()
}
UpdateMessage::Presence { from, presence } => {
- self.presences.insert(from, presence);
+ // TODO: presence handling
+ info!("got presence from {:?} {:?}", from, presence);
+ self.presences.insert(from.as_bare(), presence);
Task::none()
}
- UpdateMessage::Message { to, message } => {
- if let Some((chat_jid, (chat, old_message))) =
- self.chats.shift_remove_entry(&to)
+ UpdateMessage::Message { to, message, from } => {
+ let message = MacawMessage {
+ inner: message,
+ from: MacawUser { inner: from },
+ };
+ if let Some((chat_jid, mut chat_list_item)) =
+ self.chats_list.shift_remove_entry(&to)
{
- self.chats
- .insert_before(0, chat_jid, (chat, Some(message.clone())));
+ chat_list_item.latest_message = Some(message.clone());
+ self.chats_list.insert_before(0, chat_jid, chat_list_item);
if let Some(open_chat) = &mut self.open_chat {
- if open_chat.jid == to {
+ if open_chat.chat.user.jid == to {
open_chat.update(message_view::Message::Message(message));
}
}
} else {
+ // TODO: get the actual chat from the thing, or send the chat first, from the client side.
let chat = Chat {
correspondent: to.clone(),
+ have_chatted: false,
+ };
+ let chat_list_item = ChatListItem {
+ inner: MacawChat {
+ inner: chat,
+ user: message.from.clone(),
+ },
+ latest_message: Some(message),
};
- let message_history = indexmap! {message.id => message.clone()};
- self.chats.insert_before(0, to, (chat, Some(message)));
+ self.chats_list.insert_before(0, to, chat_list_item);
}
Task::none()
}
@@ -515,6 +804,62 @@ impl Macaw {
// TODO: subscription requests
Task::none()
}
+ UpdateMessage::MessageDelivery { chat, id, delivery } => {
+ if let Some(chat_list_item) = self.chats_list.get_mut(&chat) {
+ if let Some(latest_message) = &mut chat_list_item.latest_message {
+ if latest_message.id == id {
+ latest_message.delivery = Some(delivery)
+ }
+ }
+ }
+ if let Some(open_chat) = &mut self.open_chat {
+ if let Some(message) = open_chat.messages.get_mut(&id) {
+ message.delivery = Some(delivery)
+ }
+ }
+ Task::none()
+ }
+ UpdateMessage::NickChanged { jid, nick } => {
+ // roster, chats_list, open chat
+ if let Some(contact) = self.roster.get_mut(&jid) {
+ contact.user.nick = nick.clone();
+ }
+ if let Some(chats_list_item) = self.chats_list.get_mut(&jid) {
+ chats_list_item.user.nick = nick.clone()
+ }
+ if let Some(open_chat) = &mut self.open_chat {
+ for (_, message) in &mut open_chat.messages {
+ if message.from.jid == jid {
+ message.from.nick = nick.clone()
+ }
+ }
+ if open_chat.chat.user.jid == jid {
+ open_chat.chat.user.nick = nick
+ }
+ }
+ Task::none()
+ }
+ UpdateMessage::AvatarChanged { jid, id } => {
+ // roster, chats_list, open chat
+ if let Some(contact) = self.roster.get_mut(&jid) {
+ contact.user.avatar = id.clone();
+ }
+ if let Some(chats_list_item) = self.chats_list.get_mut(&jid) {
+ chats_list_item.user.avatar = id.clone()
+ }
+ if let Some(open_chat) = &mut self.open_chat {
+ // TODO: consider using an indexmap with two keys for speeding this up?
+ for (_, message) in &mut open_chat.messages {
+ if message.from.jid == jid {
+ message.from.avatar = id.clone()
+ }
+ }
+ if open_chat.chat.user.jid == jid {
+ open_chat.chat.user.avatar = id
+ }
+ }
+ Task::none()
+ }
},
// TODO: NEXT
Message::ClientCreated(client) => {
@@ -523,19 +868,29 @@ 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, user) in roster {
+ macaw_roster.insert(
+ contact.user_jid.clone(),
+ MacawContact {
+ inner: contact,
+ user,
+ },
+ );
+ }
+ // TODO: clean this up
+ Message::Roster(macaw_roster)
+ },
+ ),
Task::perform(
async move {
client2
.client
- .get_chats_ordered_with_latest_messages()
+ .get_chats_ordered_with_latest_messages_and_users()
.await
},
|chats| {
@@ -552,19 +907,28 @@ 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, user) in roster {
+ macaw_roster.insert(
+ contact.user_jid.clone(),
+ MacawContact {
+ inner: contact,
+ user,
+ },
+ );
+ }
+ Message::Roster(macaw_roster)
+ },
+ ),
Task::perform(
async move {
client2
.client
- .get_chats_ordered_with_latest_messages()
+ .get_chats_ordered_with_latest_messages_and_users()
.await
},
|chats| {
@@ -589,7 +953,7 @@ impl Macaw {
client.connection_state = ConnectionState::Connecting;
let client = client.client.clone();
Task::future(async move {
- client.send(CommandMessage::Connect).await;
+ client.connect().await.unwrap();
})
.discard()
}
@@ -599,9 +963,7 @@ impl Macaw {
Account::LoggedIn(client) => {
let client = client.client.clone();
Task::future(async move {
- client
- .send(CommandMessage::Disconnect(Offline::default()))
- .await;
+ client.disconnect(Offline::default()).await.unwrap();
})
.discard()
}
@@ -610,29 +972,43 @@ 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_list.get(&jid) {
+ match &self.client {
+ Account::LoggedIn(client) => {
+ let client = client.clone();
+ self.open_chat = Some(MessageView::new(
+ (*chat).clone(),
+ &self.config,
+ client.files_root.clone(),
+ ));
+ Task::perform(
+ async move { client.get_messages_with_users(jid).await },
+ move |result| {
+ let message_history = result.unwrap();
+ let messages = message_history
+ .into_iter()
+ .map(|(message, user)| MacawMessage {
+ inner: message,
+ from: MacawUser { inner: user },
+ })
+ .collect();
+ Message::MessageView(message_view::Message::MessageHistory(
+ messages,
+ ))
+ },
+ )
+ }
+ Account::LoggedOut(login_modal) => Task::none(),
}
- Account::LoggedOut(login_modal) => Task::none(),
+ } else {
+ Task::none()
}
}
Message::LoginModal(login_modal_message) => match &mut self.client {
@@ -649,9 +1025,9 @@ impl Macaw {
Ok(jid) => {
Task::perform(async move {
let (jid, creds, config) = (jid, creds, config);
- let (handle, recv) = luz(&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(
@@ -660,6 +1036,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);
@@ -719,45 +1096,25 @@ impl Macaw {
return Task::none();
}
};
- for chat in chats {
- 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)
- // },
- // ))
+ for ((chat, chat_user), (message, message_user)) in chats {
+ let chat = MacawChat {
+ inner: chat,
+ user: MacawUser { inner: chat_user },
+ };
+ let latest_message = MacawMessage {
+ inner: message,
+ from: MacawUser {
+ inner: message_user,
+ },
+ };
+ let chat_list_item = ChatListItem {
+ inner: chat.clone(),
+ latest_message: Some(latest_message),
+ };
+ self.chats_list
+ .insert(chat.correspondent.clone(), chat_list_item);
}
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 {
@@ -767,9 +1124,11 @@ impl Macaw {
return Task::none();
}
};
- Task::future(
- async move { client.send_message(jid, luz::chat::Body { body }).await },
- )
+ Task::future(async move {
+ client
+ .send_message(jid, filamento::chat::Body { body })
+ .await
+ })
.discard()
}
Message::Error(error) => {
@@ -782,7 +1141,7 @@ impl Macaw {
match action {
message_view::Action::None => Task::none(),
message_view::Action::SendMessage(m) => {
- Task::done(Message::SendMessage(message_view.jid.clone(), m))
+ Task::done(Message::SendMessage(message_view.chat.user.jid.clone(), m))
}
}
} else {
@@ -809,15 +1168,23 @@ 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_list {
+ 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(
+ &self.presences,
+ &self.roster,
+ client.files_root(),
+ chat,
+ 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)
@@ -881,23 +1248,15 @@ impl Macaw {
let extended = Extended {
background: Background {
base: Pair {
- color: color!(0x392c25),
+ color: color!(0x503e34),
text: color!(0xdcdcdc),
},
- weakest: Pair {
- color: color!(0xdcdcdc),
- text: color!(0x392c25),
- },
weak: Pair {
- color: color!(0xdcdcdc),
- text: color!(0x392c25),
- },
- strong: Pair {
- color: color!(0x364b3b),
+ color: color!(0x392c25),
text: color!(0xdcdcdc),
},
- strongest: Pair {
- color: color!(0x364b3b),
+ strong: Pair {
+ color: color!(0x293f2e),
text: color!(0xdcdcdc),
},
},
@@ -918,15 +1277,15 @@ impl Macaw {
secondary: Secondary {
base: Pair {
color: color!(0xffce07),
- text: color!(0x000000),
+ text: color!(0xdcdcdc),
},
weak: Pair {
color: color!(0xffce07),
- text: color!(0x000000),
+ text: color!(0xdcdcdc),
},
strong: Pair {
color: color!(0xffce07),
- text: color!(0x000000),
+ text: color!(0xdcdcdc),
},
},
success: Success {
@@ -943,20 +1302,6 @@ impl Macaw {
text: color!(0xdcdcdc),
},
},
- warning: Warning {
- base: Pair {
- color: color!(0xFF9D00),
- text: color!(0x000000),
- },
- weak: Pair {
- color: color!(0xFF9D00),
- text: color!(0x000000),
- },
- strong: Pair {
- color: color!(0xFF9D00),
- text: color!(0x000000),
- },
- },
danger: Danger {
base: Pair {
color: color!(0xC1173C),
@@ -1019,34 +1364,109 @@ where
}
fn chat_list_item<'a>(
- chat: &'a Chat,
- latest_message: &'a Option<ChatMessage>,
+ presences: &HashMap<JID, Presence>,
+ roster: &HashMap<JID, MacawContact>,
+ file_root: &'a Path,
+ chat_list_item: &'a ChatListItem,
open: bool,
) -> Element<'a, Message> {
- let mut content: Column<Message> = column![text(chat.correspondent.to_string())];
- if let Some(latest_message) = latest_message {
+ let name: String;
+ if let Some(Some(contact_name)) = roster
+ .get(chat_list_item.correspondent())
+ .map(|contact| &contact.name)
+ {
+ name = contact_name.clone()
+ } else if let Some(nick) = &chat_list_item.user.nick {
+ name = nick.clone()
+ } else {
+ name = chat_list_item.correspondent().to_string();
+ }
+
+ let avatar: Option<String>;
+ if let Some(user_avatar) = &chat_list_item.user.avatar {
+ avatar = Some(user_avatar.clone())
+ } else {
+ avatar = None
+ }
+
+ let latest_message_text: Option<(String, String)>;
+ if let Some(latest_message) = &chat_list_item.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(
- row![
- container(text(message).wrapping(Wrapping::None))
- .clip(true)
- .width(Fill),
- timeinfo
+ 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 mut avatar_stack = stack([]);
+ if let Some(avatar) = avatar {
+ let mut path = file_root.join(avatar);
+ path.set_extension("jpg");
+ info!("got avatar: {:?}", path);
+ avatar_stack = avatar_stack.push(image(path).width(48).height(48));
+ }
+ let mut status_icon: Option<Icon> = None;
+ if let Some(presence) = presences.get(&chat_list_item.user.jid) {
+ debug!("found a presence");
+ match &presence.presence {
+ PresenceType::Online(online) => match online.show {
+ Some(s) => match s {
+ filamento::presence::Show::Away => status_icon = Some(Icon::Away16Color),
+ filamento::presence::Show::Chat => status_icon = Some(Icon::Bubble16Color),
+ filamento::presence::Show::DoNotDisturb => status_icon = Some(Icon::Dnd16Color),
+ filamento::presence::Show::ExtendedAway => {
+ status_icon = Some(Icon::Away16Color)
+ }
+ },
+ None => status_icon = Some(Icon::Bubble16Color),
+ },
+ PresenceType::Offline(offline) => {}
+ }
+ }
+ if let Some(status_icon) = status_icon {
+ avatar_stack = avatar_stack.push(Into::<Svg>::into(status_icon));
+ }
+ let content: Element<Message> = if let Some((message, time)) = latest_message_text {
+ row![
+ avatar_stack,
+ column![
+ text(name),
+ row![
+ container(text(message).wrapping(Wrapping::None))
+ .clip(true)
+ .width(Fill),
+ text(time)
+ ]
+ .spacing(8)
+ .width(Fill)
]
.spacing(8)
- .width(Fill),
- );
- }
- let mut button = button(content).on_press(Message::ToggleChat(chat.correspondent.clone()));
+ ]
+ .spacing(8)
+ .into()
+ } else {
+ row![avatar_stack, text(name)].spacing(8).into()
+ };
+ let mut button =
+ button(content).on_press(Message::ToggleChat(chat_list_item.correspondent().clone()));
if open {
button = button.style(|theme: &Theme, status| {
let palette = theme.extended_palette();