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::{Path, PathBuf};
use std::rc::{Rc, Weak};
use std::str::FromStr;
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};
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, 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, Font, Subscription, Task, Theme};
use icons::Icon;
use indexmap::{indexmap, IndexMap};
use jid::JID;
use keyring::Entry;
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::{debug, error, info};
use uuid::Uuid;
mod icons;
mod login_modal;
mod message_view;
#[derive(Serialize, Deserialize, Clone)]
pub struct Config {
auto_connect: bool,
storage_dir: Option<String>,
dburl: Option<String>,
message_view_config: message_view::Config,
}
impl Default for Config {
fn default() -> Self {
Self {
auto_connect: true,
storage_dir: None,
dburl: None,
message_view_config: message_view::Config::default(),
}
}
}
// 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,
presences: HashMap<JID, Presence>,
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;
impl Macaw {
pub fn new(client: Option<Client>, config: Config) -> Self {
let account;
if let Some(client) = client {
account = Account::LoggedIn(client);
} else {
account = Account::LoggedOut(LoginModal::default());
}
Self {
client: account,
config,
presences: HashMap::new(),
subscription_requests: HashSet::new(),
new_chat: None,
open_chat: None,
roster: HashMap::new(),
chats_list: IndexMap::new(),
}
}
}
pub enum Account {
LoggedIn(Client),
LoggedOut(LoginModal),
}
impl Account {
pub fn is_connected(&self) -> bool {
match self {
Account::LoggedIn(client) => client.connection_state.is_connected(),
Account::LoggedOut(login_modal) => false,
}
}
pub fn connection_status(&self) -> String {
match self {
Account::LoggedIn(client) => match client.connection_state {
ConnectionState::Online => "online".to_string(),
ConnectionState::Connecting => "connecting".to_string(),
ConnectionState::Offline => "offline".to_string(),
},
Account::LoggedOut(login_modal) => "no account".to_string(),
}
}
}
#[derive(Clone, Debug)]
pub struct Client {
client: filamento::Client<Files>,
files_root: PathBuf,
jid: JID,
status: Presence,
connection_state: ConnectionState,
}
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)]
pub enum ConnectionState {
Online,
Connecting,
Offline,
}
impl ConnectionState {
pub fn is_connected(&self) -> bool {
match self {
ConnectionState::Online => true,
ConnectionState::Connecting => false,
ConnectionState::Offline => false,
}
}
}
impl DerefMut for Client {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.client
}
}
impl Deref for Client {
type Target = filamento::Client<Files>;
fn deref(&self) -> &Self::Target {
&self.client
}
}
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 = filamento::db::Db::create_connect_and_migrate(db_path)
.await
.unwrap();
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 {
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
}
#[tokio::main]
async fn main() -> iced::Result {
tracing_subscriber::fmt::init();
let cfg: Config = confy::load("macaw", None).unwrap_or_default();
let entry = Entry::new("macaw", "macaw");
let mut client_creation_error: Option<Error> = None;
let mut creds: Option<Creds> = None;
match entry {
Ok(e) => {
let result = e.get_password();
match result {
Ok(c) => {
let result = toml::from_str(&c);
match result {
Ok(c) => creds = Some(c),
Err(e) => {
client_creation_error =
Some(Error::CredentialsLoad(CredentialsLoadError::Toml(e.into())))
}
}
}
Err(e) => match e {
keyring::Error::NoEntry => {}
_ => {
client_creation_error = Some(Error::CredentialsLoad(
CredentialsLoadError::Keyring(e.into()),
))
}
},
}
}
Err(e) => {
client_creation_error = Some(Error::CredentialsLoad(CredentialsLoadError::Keyring(
e.into(),
)))
}
}
let mut client: Option<(
JID,
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), 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, 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_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_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)
},
),
Task::stream(stream),
])
}
};
let mut font = Font::with_name("K2D");
font.weight = Weight::Light;
// 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(|| {
(
Macaw::new(
Some(Client {
client: luz_handle,
jid,
status: Presence {
timestamp: Utc::now(),
presence: PresenceType::Offline(Offline::default()),
},
connection_state: ConnectionState::Offline,
files_root,
}),
cfg,
),
task,
)
})
} 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()))
}
}
}
fn subscription(state: &Macaw) -> Subscription<Message> {
Subscription::batch([press_subscription(state), release_subscription(state)])
}
fn press_subscription(_state: &Macaw) -> Subscription<Message> {
on_key_press(handle_key_press)
}
fn handle_key_press(key: Key, r#mod: Modifiers) -> Option<Message> {
match key {
Key::Named(iced::keyboard::key::Named::Shift) => Some(Message::ShiftPressed),
_ => None,
}
}
fn release_subscription(_state: &Macaw) -> Subscription<Message> {
on_key_release(handle_key_release)
}
fn handle_key_release(key: Key, r#mod: Modifiers) -> Option<Message> {
match key {
Key::Named(iced::keyboard::key::Named::Shift) => Some(Message::ShiftReleased),
_ => None,
}
}
#[derive(Debug, Clone)]
pub enum Message {
ShiftPressed,
ShiftReleased,
LoginModal(login_modal::Message),
ClientCreated(Client),
Luz(UpdateMessage),
Roster(HashMap<JID, MacawContact>),
Connect,
Disconnect,
GotChats(Vec<((Chat, User), (ChatMessage, User))>),
ToggleChat(JID),
SendMessage(JID, String),
Error(Error),
MessageView(message_view::Message),
}
#[derive(Debug, Error, Clone)]
pub enum Error {
#[error("failed to create Luz client: {0}")]
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<filamento::error::DatabaseError>),
}
#[derive(Debug, Error, Clone)]
pub enum CredentialsSaveError {
#[error("keyring: {0}")]
Keyring(Arc<keyring::Error>),
#[error("toml serialisation: {0}")]
Toml(#[from] toml::ser::Error),
}
impl From<keyring::Error> for CredentialsSaveError {
fn from(e: keyring::Error) -> Self {
Self::Keyring(Arc::new(e))
}
}
#[derive(Debug, Error, Clone)]
pub enum CredentialsLoadError {
#[error("keyring: {0}")]
Keyring(Arc<keyring::Error>),
#[error("toml serialisation: {0}")]
Toml(#[from] toml::de::Error),
#[error("invalid jid: {0}")]
JID(#[from] jid::ParseError),
}
impl From<keyring::Error> for CredentialsLoadError {
fn from(e: keyring::Error) -> Self {
Self::Keyring(Arc::new(e))
}
}
impl Macaw {
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::Luz(update_message) => match update_message {
UpdateMessage::Online(online, vec) => match &mut self.client {
Account::LoggedIn(client) => {
client.status = Presence {
timestamp: Utc::now(),
presence: PresenceType::Online(online),
};
client.connection_state = ConnectionState::Online;
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()
}
Account::LoggedOut(login_modal) => Task::none(),
},
UpdateMessage::Offline(offline) => {
// TODO: update all contacts' presences to unknown (offline)
match &mut self.client {
Account::LoggedIn(client) => {
client.status = Presence {
timestamp: Utc::now(),
presence: PresenceType::Offline(offline),
};
client.connection_state = ConnectionState::Offline;
Task::none()
}
Account::LoggedOut(login_modal) => Task::none(),
}
}
UpdateMessage::RosterUpdate(contact, user) => {
self.roster.insert(
contact.user_jid.clone(),
MacawContact {
inner: contact,
user,
},
);
Task::none()
}
UpdateMessage::RosterDelete(jid) => {
self.roster.remove(&jid);
Task::none()
}
UpdateMessage::Presence { from, presence } => {
// TODO: presence handling
info!("got presence from {:?} {:?}", from, presence);
self.presences.insert(from.as_bare(), presence);
Task::none()
}
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)
{
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.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),
};
self.chats_list.insert_before(0, to, chat_list_item);
}
Task::none()
}
UpdateMessage::SubscriptionRequest(jid) => {
// 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) => {
self.client = Account::LoggedIn(client.clone());
let client1 = client.clone();
let client2 = client.clone();
if self.config.auto_connect {
Task::batch([
Task::perform(
async move { client1.client.get_roster_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_and_users()
.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();
info!("got chats: {:?}", chats);
Message::GotChats(chats)
},
),
])
.chain(Task::done(Message::Connect))
} else {
Task::batch([
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_and_users()
.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();
info!("got chats: {:?}", chats);
Message::GotChats(chats)
},
),
])
}
}
Message::Roster(hash_map) => {
self.roster = hash_map;
Task::none()
}
Message::Connect => match &mut self.client {
Account::LoggedIn(client) => {
client.connection_state = ConnectionState::Connecting;
let client = client.client.clone();
Task::future(async move {
client.connect().await.unwrap();
})
.discard()
}
Account::LoggedOut(login_modal) => Task::none(),
},
Message::Disconnect => match &self.client {
Account::LoggedIn(client) => {
let client = client.client.clone();
Task::future(async move {
client.disconnect(Offline::default()).await.unwrap();
})
.discard()
}
Account::LoggedOut(login_modal) => Task::none(),
},
Message::ToggleChat(jid) => {
match &self.open_chat {
Some(message_view) => {
if message_view.chat.user.jid == jid {
self.open_chat = None;
return Task::none();
}
}
None => {}
}
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(),
}
} else {
Task::none()
}
}
Message::LoginModal(login_modal_message) => match &mut self.client {
Account::LoggedIn(_client) => Task::none(),
Account::LoggedOut(login_modal) => {
let action = login_modal.update(login_modal_message);
match action {
login_modal::Action::None => Task::none(),
login_modal::Action::CreateClient(jid, password, remember_me) => {
let creds = Creds { jid, password };
let jid = creds.jid.parse::<JID>();
let config = self.config.clone();
match jid {
Ok(jid) => {
Task::perform(async move {
let (jid, creds, config) = (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(
Client {
client: handle,
jid,
status: Presence { timestamp: Utc::now(), presence: PresenceType::Offline(Offline::default()) },
connection_state: ConnectionState::Offline,
files_root,
},
)));
let stream = ReceiverStream::new(recv);
let stream =
stream.map(|message| crate::Message::Luz(message));
tasks.push(Task::stream(stream));
if remember_me {
let entry = Entry::new("macaw", "macaw");
match entry {
Ok(e) => {
let creds = toml::to_string(&creds);
match creds {
Ok(c) => {
let result = e.set_password(&c);
if let Err(e) = result {
tasks.push(Task::done(crate::Message::Error(
crate::Error::CredentialsSave(e.into()),
)));
}
}
Err(e) => tasks.push(Task::done(
crate::Message::Error(
crate::Error::CredentialsSave(
e.into(),
),
),
)),
}
}
Err(e) => {
tasks.push(Task::done(crate::Message::Error(
crate::Error::CredentialsSave(e.into()),
)))
}
}
}
tasks
}).then(|tasks| Task::batch(tasks))
}
Err(e) => Task::done(Message::LoginModal(
login_modal::Message::Error(login_modal::Error::InvalidJID),
)),
}
}
login_modal::Action::ClientCreated(task) => task,
}
}
},
Message::GotChats(chats) => {
let mut tasks = Vec::new();
let client = match &self.client {
Account::LoggedIn(client) => client,
Account::LoggedOut(_) => {
// TODO: error into event tracing subscriber
error!("no client, cannot retreive chat history for chats");
return Task::none();
}
};
for ((chat, 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)
}
Message::SendMessage(jid, body) => {
let client = match &self.client {
Account::LoggedIn(client) => client.clone(),
Account::LoggedOut(_) => {
error!("cannot send message when no client set up");
return Task::none();
}
};
Task::future(async move {
client
.send_message(jid, filamento::chat::Body { body })
.await
})
.discard()
}
Message::Error(error) => {
error!("{}", error);
Task::none()
}
Message::MessageView(message) => {
if let Some(message_view) = &mut self.open_chat {
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.chat.user.jid.clone(), m))
}
}
} else {
Task::none()
}
}
Message::ShiftPressed => {
info!("shift pressed");
if let Some(open_chat) = &mut self.open_chat {
open_chat.shift_pressed = true;
}
Task::none()
}
Message::ShiftReleased => {
info!("shift released");
if let Some(open_chat) = &mut self.open_chat {
open_chat.shift_pressed = false;
}
Task::none()
}
}
}
fn view(&self) -> Element<Message> {
let mut ui: Element<Message> = {
let mut chats_list: Column<Message> = column![];
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 chats_list = scrollable(chats_list.spacing(8).padding(8))
.spacing(1)
.height(Fill);
let connection_status = self.client.connection_status();
let client_jid: Cow<'_, str> = match &self.client {
Account::LoggedIn(client) => (&client.jid).into(),
Account::LoggedOut(_) => Cow::from("no account"),
// map(|client| (&client.jid).into());
};
let connected = self.client.is_connected();
let account_view = container(row![
text(client_jid),
horizontal_space(),
text(connection_status),
horizontal_space().width(8),
toggler(connected).on_toggle(|connect| {
if connect {
Message::Connect
} else {
Message::Disconnect
}
})
])
.padding(8);
// TODO: config width/resizing
let sidebar = column![chats_list, account_view].height(Fill).width(300);
let message_view;
if let Some(open_chat) = &self.open_chat {
message_view = open_chat.view().map(Message::MessageView)
} else {
message_view = column![].into();
}
row![sidebar, container(message_view).width(Fill)]
}
.into();
if let Some(new_chat) = &self.new_chat {
// TODO: close new chat window
ui = modal(ui, text("new chat"), None);
}
// temporarily center to fill space
// let ui = center(ui).into();
let ui = container(ui).center_x(Fill).center_y(Fill);
match &self.client {
Account::LoggedIn(_client) => ui.into(),
Account::LoggedOut(login_modal) => {
let signup = login_modal.view().map(Message::LoginModal);
modal(ui, signup, None)
}
}
}
fn theme(&self) -> Theme {
let extended = Extended {
background: Background {
base: Pair {
color: color!(0x503e34),
text: color!(0xdcdcdc),
},
weak: Pair {
color: color!(0x392c25),
text: color!(0xdcdcdc),
},
strong: Pair {
color: color!(0x293f2e),
text: color!(0xdcdcdc),
},
},
primary: Primary {
base: Pair {
color: color!(0x2b33b4),
text: color!(0xdcdcdc),
},
weak: Pair {
color: color!(0x4D4A5E),
text: color!(0xdcdcdc),
},
strong: Pair {
color: color!(0x2b33b4),
text: color!(0xdcdcdc),
},
},
secondary: Secondary {
base: Pair {
color: color!(0xffce07),
text: color!(0xdcdcdc),
},
weak: Pair {
color: color!(0xffce07),
text: color!(0xdcdcdc),
},
strong: Pair {
color: color!(0xffce07),
text: color!(0xdcdcdc),
},
},
success: Success {
base: Pair {
color: color!(0x14802E),
text: color!(0xdcdcdc),
},
weak: Pair {
color: color!(0x14802E),
text: color!(0xdcdcdc),
},
strong: Pair {
color: color!(0x14802E),
text: color!(0xdcdcdc),
},
},
danger: Danger {
base: Pair {
color: color!(0xC1173C),
text: color!(0xdcdcdc),
},
weak: Pair {
color: color!(0xC1173C),
text: color!(0xdcdcdc),
},
strong: Pair {
color: color!(0xC1173C),
text: color!(0xdcdcdc),
},
},
is_dark: true,
};
Theme::Custom(Arc::new(Custom::with_fn(
"macaw".to_string(),
Palette::DARK,
|_| extended,
)))
// Theme::Custom(Arc::new(Custom::new(
// "macaw".to_string(),
// Palette {
// background: color!(0x392c25),
// text: color!(0xdcdcdc),
// primary: color!(0x2b33b4),
// success: color!(0x14802e),
// warning: color!(0xffce07),
// danger: color!(0xc1173c),
// },
// )))
}
}
fn modal<'a, Message>(
base: impl Into<Element<'a, Message>>,
content: impl Into<Element<'a, Message>>,
on_blur: Option<Message>,
) -> Element<'a, Message>
where
Message: Clone + 'a,
{
let mut mouse_area = mouse_area(center(opaque(content)).style(|_theme| {
container::Style {
background: Some(
Color {
a: 0.8,
..Color::BLACK
}
.into(),
),
..container::Style::default()
}
})); // .on_press(on_blur)
if let Some(on_blur) = on_blur {
mouse_area = mouse_area.on_press(on_blur)
}
stack![base.into(), opaque(mouse_area)].into()
}
fn chat_list_item<'a>(
presences: &HashMap<JID, Presence>,
roster: &HashMap<JID, MacawContact>,
file_root: &'a Path,
chat_list_item: &'a ChatListItem,
open: bool,
) -> Element<'a, 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 = date.time().format("%H:%M").to_string()
} else {
timeinfo = date.date().format("%d/%m").to_string()
}
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)
]
.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();
button::Style::default().with_background(palette.primary.weak.color)
});
}
button.width(Fill).into()
}