use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::ops::{Deref, DerefMut};
use iced::futures::{SinkExt, Stream, StreamExt};
use iced::widget::button::Status;
use iced::widget::text::{Fragment, IntoFragment};
use iced::widget::{
button, center, checkbox, column, container, mouse_area, opaque, row, scrollable, stack, text,
text_input, Column, Text, Toggler,
};
use iced::Length::Fill;
use iced::{stream, Color, Element, Subscription, Task, Theme};
use indexmap::{indexmap, IndexMap};
use jid::JID;
use keyring::Entry;
use login_modal::LoginModal;
use luz::chat::{Chat, Message as ChatMessage};
use luz::presence::{Offline, Presence};
use luz::CommandMessage;
use luz::{roster::Contact, user::User, LuzHandle, UpdateMessage};
use serde::{Deserialize, Serialize};
use tokio::sync::{mpsc, oneshot};
use tokio_stream::wrappers::ReceiverStream;
use tracing::{error, info};
use uuid::Uuid;
mod login_modal;
#[derive(Serialize, Deserialize)]
pub struct Config {
auto_connect: bool,
}
impl Default for Config {
fn default() -> Self {
Self { auto_connect: true }
}
}
pub struct Macaw {
client: Account,
config: Config,
roster: HashMap<JID, Contact>,
users: HashMap<JID, User>,
presences: HashMap<JID, Presence>,
chats: IndexMap<JID, (Chat, IndexMap<Uuid, ChatMessage>)>,
subscription_requests: HashSet<JID>,
open_chat: Option<OpenChat>,
new_chat: Option<NewChat>,
}
pub struct OpenChat {
jid: JID,
new_message: String,
}
pub struct NewChat;
pub struct Creds {
jid: String,
password: String,
}
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(),
presences: HashMap::new(),
chats: IndexMap::new(),
subscription_requests: HashSet::new(),
open_chat: None,
new_chat: None,
}
}
}
pub enum Account {
LoggedIn(Client),
LoggedOut(LoginModal),
}
#[derive(Clone, Debug)]
pub struct Client {
client: LuzHandle,
jid: JID,
connection_status: Presence,
}
impl DerefMut for Client {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.client
}
}
impl Deref for Client {
type Target = LuzHandle;
fn deref(&self) -> &Self::Target {
&self.client
}
}
fn main() -> iced::Result {
tracing_subscriber::fmt::init();
let cfg = confy::load("macaw", None).unwrap();
let client: Option<(JID, LuzHandle, mpsc::Receiver<UpdateMessage>)> = None;
if let Some((jid, luz_handle, update_recv)) = client {
let stream = ReceiverStream::new(update_recv);
let stream = stream.map(|message| Message::Luz(message));
iced::application("Macaw", Macaw::update, Macaw::view).run_with(|| {
(
Macaw::new(
Some(Client {
client: luz_handle,
// TODO:
jid,
connection_status: Presence::Offline(Offline::default()),
}),
cfg,
),
// TODO: autoconnect config
Task::stream(stream),
)
})
} else {
iced::application("Macaw", Macaw::update, Macaw::view)
.run_with(|| (Macaw::new(None, cfg), Task::none()))
}
}
#[derive(Debug, Clone)]
pub enum Message {
LoginModal(login_modal::Message),
ClientCreated(Client),
Luz(UpdateMessage),
Roster(HashMap<JID, Contact>),
Connect,
Disconnect,
OpenChat(JID),
GotChats(Vec<Chat>),
GotMessageHistory(Chat, IndexMap<Uuid, ChatMessage>),
CloseChat(JID),
MessageCompose(String),
SendMessage(JID, String),
}
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.connection_status = Presence::Online(online);
let mut roster = HashMap::new();
for contact in vec {
roster.insert(contact.user_jid.clone(), contact);
}
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.connection_status = Presence::Offline(offline);
Task::none()
}
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);
Task::none()
}
UpdateMessage::RosterDelete(jid) => {
self.roster.remove(&jid);
Task::none()
}
UpdateMessage::Presence { from, presence } => {
self.presences.insert(from, presence);
Task::none()
}
UpdateMessage::Message { to, message } => {
if let Some((_chat, message_history)) = self.chats.get_mut(&to) {
message_history.insert(message.id, message);
} else {
let chat = Chat {
correspondent: to.clone(),
};
let message_history = indexmap! {message.id => message};
self.chats.insert(to, (chat, message_history));
}
Task::none()
}
UpdateMessage::SubscriptionRequest(jid) => {
// TODO: subscription requests
Task::none()
}
},
// TODO: NEXT
Message::ClientCreated(client) => {
self.client = Account::LoggedIn(client.clone());
let client1 = client.clone();
let client2 = client.clone();
Task::batch([
Task::perform(async move { client1.client.get_roster().await }, |result| {
let roster = result.unwrap();
let mut macaw_roster = HashMap::new();
for contact in roster {
macaw_roster.insert(contact.user_jid.clone(), contact);
}
Message::Roster(macaw_roster)
}),
Task::perform(async move { client2.client.get_chats().await }, |chats| {
let chats = chats.unwrap();
// let chats: HashMap<JID, (Chat, IndexMap<Uuid, ChatMessage>)> = chats
// .into_iter()
// .map(|chat| (chat.correspondent.clone(), (chat, IndexMap::new())))
// .collect();
Message::GotChats(chats)
}),
])
}
Message::Roster(hash_map) => {
self.roster = hash_map;
Task::none()
}
Message::Connect => match &self.client {
Account::LoggedIn(client) => {
let client = client.client.clone();
Task::future(async move {
client.send(CommandMessage::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
.send(CommandMessage::Disconnect(Offline::default()))
.await;
})
.discard()
}
Account::LoggedOut(login_modal) => Task::none(),
},
Message::OpenChat(jid) => {
self.open_chat = Some(OpenChat {
jid,
new_message: String::new(),
});
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::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 in chats {
let client = client.clone();
let correspondent = chat.correspondent.clone();
tasks.push(Task::perform(
async move { (chat, client.get_messages(correspondent).await) },
|result| {
let messages: IndexMap<Uuid, ChatMessage> = result
.1
.unwrap()
.into_iter()
.map(|message| (message.id.clone(), message))
.collect();
Message::GotMessageHistory(result.0, messages)
},
))
}
Task::batch(tasks)
// .then(|chats| {
// let tasks = Vec::new();
// for key in chats.keys() {
// let client = client.client.clone();
// tasks.push(Task::future(async {
// client.get_messages(key.clone()).await;
// }));
// }
// Task::batch(tasks)
// }),
}
Message::GotMessageHistory(chat, message_history) => {
self.chats
.insert(chat.correspondent.clone(), (chat, message_history));
Task::none()
}
Message::CloseChat(jid) => {
self.open_chat = None;
Task::none()
}
Message::MessageCompose(m) => {
if let Some(open_chat) = &mut self.open_chat {
open_chat.new_message = m;
}
Task::none()
}
Message::SendMessage(jid, body) => {
let client = match &self.client {
Account::LoggedIn(client) => client.clone(),
Account::LoggedOut(_) => {
error!("cannot send message when no client set up");
return Task::none();
}
};
Task::future(
async move { client.send_message(jid, luz::chat::Body { body }).await },
)
.discard()
}
}
}
fn view(&self) -> Element<Message> {
let mut ui: Element<Message> = {
let mut chats_list: Column<Message> = column![];
for (jid, chat) in &self.chats {
let cow_jid: Cow<'_, str> = (jid).into();
let mut toggler: Toggler<Message> = iced::widget::toggler(false);
if let Some(open_chat) = &self.open_chat {
if open_chat.jid == *jid {
toggler = iced::widget::toggler(true)
}
}
let toggler = toggler
.on_toggle(|open| {
if open {
Message::OpenChat(jid.clone())
} else {
Message::CloseChat(jid.clone())
}
})
.label(cow_jid);
chats_list = chats_list.push(toggler);
}
let chats_list = scrollable(chats_list).height(Fill);
let connection_status = match &self.client {
Account::LoggedIn(client) => match &client.connection_status {
Presence::Online(_online) => "online",
Presence::Offline(_offline) => "disconnected",
},
Account::LoggedOut(_) => "disconnected",
};
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 account_view = row![
text(client_jid),
text(connection_status),
button("connect").on_press(Message::Connect),
button("disconnect").on_press(Message::Disconnect)
];
let sidebar = column![chats_list, account_view].height(Fill);
let message_view;
if let Some(open_chat) = &self.open_chat {
let (chat, messages) = self.chats.get(&open_chat.jid).unwrap();
let mut messages_view = column![];
for (_id, message) in messages {
let from: Cow<'_, str> = (&message.from).into();
let message: Column<Message> =
column![text(from).size(12), text(&message.body.body)].into();
messages_view = messages_view.push(message);
}
let message_send_input = row![
text_input("new message", &open_chat.new_message)
.on_input(Message::MessageCompose),
button("send").on_press(Message::SendMessage(
chat.correspondent.clone(),
open_chat.new_message.clone()
))
];
message_view = column![
scrollable(messages_view)
.height(Fill)
.width(Fill)
.anchor_bottom(),
message_send_input
];
} else {
message_view = column![];
}
row![sidebar, message_view.width(Fill)]
// old
// let mut contacts: Vec<Element<Message>> = Vec::new();
// for (_, contact) in &self.roster {
// let jid: Cow<'_, str> = (&contact.user_jid).into();
// contacts.push(
// button(text(jid))
// .on_press(Message::OpenChat(contact.user_jid.clone()))
// .into(),
// );
// }
}
.into();
if let Some(new_chat) = &self.new_chat {
// 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 {
Theme::Dark
}
}
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()
}