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::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::{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, Text, Toggler,
};
use iced::Length::{self, Fill, Shrink};
use iced::{color, stream, Color, Element, Subscription, Task, Theme};
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::{error, info};
use uuid::Uuid;
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,
// 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 {
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,
roster: HashMap::new(),
users: HashMap::new(),
chats: IndexMap::new(),
subscription_requests: HashSet::new(),
open_chat: None,
new_chat: None,
messages: HashMap::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 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| {
let chats = chats.unwrap();
info!("got chats: {:?}", chats);
Message::GotChats(chats)
},
),
])
.chain(Task::done(Message::Connect)),
Task::stream(stream),
],
)
} else {
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 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| {
let chats = chats.unwrap();
info!("got chats: {:?}", chats);
Message::GotChats(chats)
},
),
Task::stream(stream),
])
}
};
iced::application("Macaw", Macaw::update, Macaw::view)
.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)
.run_with(|| (Macaw::new(None, cfg), Task::done(Message::Error(e))))
} else {
iced::application("Macaw", Macaw::update, Macaw::view)
.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),
RosterWithUsers(HashMap<JID, (Contact, User)>),
Connect,
Disconnect,
GotChats(Vec<(Chat, ChatMessage)>),
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 mut roster = HashMap::new();
let mut get_users = Vec::new();
for contact in vec {
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()
})
}
}
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::FullRoster(vec) => {
let mut roster = HashMap::new();
let mut get_users = Vec::new();
for contact in vec {
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()
})
}
}
UpdateMessage::RosterUpdate(contact) => {
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 } => {
// TODO: presence handling
Task::none()
}
UpdateMessage::Message { to, message } => {
if let Some(Some(user)) =
self.users.get(&message.from).map(|user| user.upgrade())
{
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 {
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(),
}
}
}
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) => {
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 in roster {
macaw_roster.insert(contact.0.user_jid.clone(), contact);
}
Message::RosterWithUsers(macaw_roster)
},
),
Task::perform(
async move {
client2
.client
.get_chats_ordered_with_latest_messages()
.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 in roster {
macaw_roster.insert(contact.0.user_jid.clone(), contact);
}
Message::RosterWithUsers(macaw_roster)
},
),
Task::perform(
async move {
client2
.client
.get_chats_ordered_with_latest_messages()
.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::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 {
Account::LoggedIn(client) => {
client.connection_state = ConnectionState::Connecting;
let client = client.client.clone();
Task::future(async move {
client.connect().await;
})
.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;
})
.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.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(),
}
} 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, message) in chats {
let chat = MacawChat {
inner: todo!(),
user: todo!(),
message: todo!(),
}
self.chats
.insert(chat.0.correspondent.clone(), (chat.0.clone(), Some(chat.1)));
}
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 {
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 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!(0x392c25),
text: color!(0xdcdcdc),
},
weakest: Pair {
color: color!(0xdcdcdc),
text: color!(0x392c25),
},
weak: Pair {
color: color!(0xdcdcdc),
text: color!(0x392c25),
},
strong: Pair {
color: color!(0x364b3b),
text: color!(0xdcdcdc),
},
strongest: Pair {
color: color!(0x364b3b),
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!(0x000000),
},
weak: Pair {
color: color!(0xffce07),
text: color!(0x000000),
},
strong: Pair {
color: color!(0xffce07),
text: color!(0x000000),
},
},
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),
},
},
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),
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, 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 = 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 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![
avatar_image,
column![
text(name),
row![
container(text(message).wrapping(Wrapping::None))
.clip(true)
.width(Fill),
text(time)
]
.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| {
let palette = theme.extended_palette();
button::Style::default().with_background(palette.primary.weak.color)
});
}
button.width(Fill).into()
}