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::text::{Fragment, IntoFragment};
use iced::widget::{
button, center, column, container, mouse_area, opaque, row, stack, text, text_input, Text,
};
use iced::{stream, Color, Element, Subscription, Task, Theme};
use jid::JID;
use luz::chat::{Chat, Message as ChatMessage};
use luz::presence::{Offline, Presence};
use luz::CommandMessage;
use luz::{roster::Contact, user::User, LuzHandle, UpdateMessage};
use tokio::sync::{mpsc, oneshot};
use tokio_stream::wrappers::ReceiverStream;
use tracing::info;
pub struct Macaw {
client: Account,
roster: HashMap<JID, Contact>,
users: HashMap<JID, User>,
presences: HashMap<JID, Presence>,
chats: HashMap<JID, (Chat, Vec<ChatMessage>)>,
subscription_requests: HashSet<JID>,
}
pub struct Creds {
jid: String,
password: String,
}
impl Macaw {
pub fn new(client: Option<Client>) -> Self {
let account;
if let Some(client) = client {
account = Account::LoggedIn(client);
} else {
account = Account::LoggedOut {
jid: "".to_string(),
password: "".to_string(),
error: None,
};
}
Self {
client: account,
roster: HashMap::new(),
users: HashMap::new(),
presences: HashMap::new(),
chats: HashMap::new(),
subscription_requests: HashSet::new(),
}
}
}
pub enum Account {
LoggedIn(Client),
LoggedOut {
jid: String,
password: String,
error: Option<Error>,
},
}
#[derive(Debug, Clone)]
pub enum Error {
InvalidJID(String),
DatabaseConnection,
}
#[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 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()),
})),
// TODO: autoconnect config
Task::stream(stream),
)
})
} else {
iced::application("Macaw", Macaw::update, Macaw::view)
.run_with(|| (Macaw::new(None), Task::none()))
}
}
#[derive(Debug, Clone)]
enum Message {
LoginModal(LoginModalMessage),
ClientCreated(Client),
Luz(UpdateMessage),
Roster(HashMap<JID, Contact>),
Connect,
Disconnect,
OpenChat(JID),
}
#[derive(Debug, Clone)]
enum LoginModalMessage {
JID(String),
Password(String),
Submit,
Error(Error),
}
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 {
jid,
password,
error,
} => 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 {
jid,
password,
error,
} => 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.push(message);
} else {
let chat = Chat {
correspondent: to.clone(),
};
let message_history = vec![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 (send, recv) = oneshot::channel();
Task::perform(
async move {
client.client.send(CommandMessage::GetRoster(send)).await;
recv.await
},
|result| {
let roster = result.unwrap().unwrap();
let mut macaw_roster = HashMap::new();
for contact in roster {
macaw_roster.insert(contact.user_jid.clone(), contact);
}
Message::Roster(macaw_roster)
},
)
}
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 {
jid,
password,
error,
} => 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 {
jid,
password,
error,
} => Task::none(),
},
Message::OpenChat(jid) => todo!(),
Message::LoginModal(login_modal_message) => match login_modal_message {
LoginModalMessage::JID(j) => match &mut self.client {
Account::LoggedIn(_client) => Task::none(),
Account::LoggedOut {
jid,
password,
error,
} => {
*jid = j;
Task::none()
}
},
LoginModalMessage::Password(p) => match &mut self.client {
Account::LoggedIn(_client) => Task::none(),
Account::LoggedOut {
jid,
password,
error,
} => {
*password = p;
Task::none()
}
},
LoginModalMessage::Submit => match &self.client {
Account::LoggedIn(_client) => Task::none(),
Account::LoggedOut {
jid: jid_str,
password,
error,
} => {
info!("submitting login");
let jid_str = jid_str.clone();
let password = password.clone();
Task::future(async move {
let jid: Result<JID, _> = jid_str.parse();
match jid {
Ok(j) => {
let result =
LuzHandle::new(j.clone(), password.to_string(), "macaw.db")
.await;
match result {
Ok((luz_handle, receiver)) => {
let stream = ReceiverStream::new(receiver);
let stream =
stream.map(|message| Message::Luz(message));
vec![
Task::done(Message::ClientCreated(Client {
client: luz_handle,
jid: j,
connection_status: Presence::Offline(
Offline::default(),
),
})),
Task::stream(stream),
]
}
Err(e) => {
tracing::error!("error (database probably)");
return vec![Task::done(Message::LoginModal(
LoginModalMessage::Error(Error::DatabaseConnection),
))];
}
}
}
Err(_) => {
tracing::error!("parsing jid");
return vec![Task::done(Message::LoginModal(
LoginModalMessage::Error(Error::InvalidJID(
jid_str.to_string(),
)),
))];
}
}
})
.then(|tasks| Task::batch(tasks))
}
},
LoginModalMessage::Error(e) => match &mut self.client {
Account::LoggedIn(_client) => Task::none(),
Account::LoggedOut {
jid,
password,
error,
} => {
tracing::error!("luz::new: {:?}", e);
*error = Some(e);
Task::none()
}
},
},
}
}
fn view(&self) -> Element<Message> {
let ui = {
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(),
);
}
let column = column(contacts);
let connection_status = match &self.client {
Account::LoggedIn(client) => match &client.connection_status {
Presence::Online(_online) => "online",
Presence::Offline(_offline) => "disconnected",
},
Account::LoggedOut {
jid: _,
password: _,
error,
} => "disconnected",
};
// match &self.client.as_ref().map(|client| &client.connection_status) {
// Some(s) => match s {
// Presence::Online(online) => "connected",
// Presence::Offline(offline) => "disconnected",
// },
// None => "no account",
// };
let client_jid: Cow<'_, str> = match &self.client {
Account::LoggedIn(client) => (&client.jid).into(),
Account::LoggedOut {
jid: _,
password: _,
error,
} => Cow::from("no account"),
// map(|client| (&client.jid).into());
};
column![
row![
text(client_jid),
text(connection_status),
button("connect").on_press(Message::Connect),
button("disconnect").on_press(Message::Disconnect)
],
text("Buddy List:"),
//
//
column,
]
};
// temporarily center to fill space
let ui = center(ui).into();
match &self.client {
Account::LoggedIn(_client) => ui,
Account::LoggedOut {
jid,
password,
error,
} => {
let signup = container(
column![
text("Log In").size(24),
column![
column![
text("JID").size(12),
text_input("berry@macaw.chat", &jid)
.on_input(|j| Message::LoginModal(LoginModalMessage::JID(j)))
.on_submit(Message::LoginModal(LoginModalMessage::Submit))
.padding(5),
]
.spacing(5),
column![
text("Password").size(12),
text_input("", &password)
.on_input(|p| Message::LoginModal(LoginModalMessage::Password(
p
)))
.on_submit(Message::LoginModal(LoginModalMessage::Submit))
.secure(true)
.padding(5),
]
.spacing(5),
button(text("Submit"))
.on_press(Message::LoginModal(LoginModalMessage::Submit)),
]
.spacing(10)
]
.spacing(20),
)
.width(300)
.padding(10)
.style(container::rounded_box);
// signup.into()
modal(ui, signup)
}
}
}
fn theme(&self) -> Theme {
Theme::Dark
}
}
fn modal<'a, Message>(
base: impl Into<Element<'a, Message>>,
content: impl Into<Element<'a, Message>>,
// on_blur: Message,
) -> Element<'a, Message>
where
Message: Clone + 'a,
{
stack![
base.into(),
opaque(
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)
)
]
.into()
}