use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::ops::{Deref, DerefMut};
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use chrono::{Local, Utc};
use filamento::chat::{Chat, Message as ChatMessage};
use filamento::error::CommandError;
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, 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, 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(),
}
}
}
pub struct Macaw {
client: Account,
config: Config,
roster: HashMap<JID, Contact>,
users: HashMap<JID, User>,
presences: HashMap<JID, Presence>,
chats: IndexMap<JID, (Chat, Option<ChatMessage>)>,
subscription_requests: HashSet<JID>,
open_chat: Option<MessageView>,
new_chat: Option<NewChat>,
}
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(),
presences: HashMap::new(),
chats: IndexMap::new(),
subscription_requests: HashSet::new(),
open_chat: None,
new_chat: None,
}
}
}
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,
jid: JID,
status: Presence,
connection_state: ConnectionState,
}
impl Client {
pub fn is_connected(&self) -> bool {
self.connection_state.is_connected()
}
}
#[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;
fn deref(&self) -> &Self::Target {
&self.client
}
}
async fn filamento(
jid: &JID,
creds: &Creds,
cfg: &Config,
) -> (filamento::Client, mpsc::Receiver<UpdateMessage>) {
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();
filamento = filamento::Client::new(jid.clone(), creds.password.to_string(), db);
} else if let Some(ref dir) = cfg.storage_dir {
let mut data_dir = PathBuf::from_str(&dir).expect("invalid storage directory path");
data_dir.push(creds.jid.clone());
data_dir.push(creds.jid.clone());
data_dir.set_extension("db");
let db = filamento::db::Db::create_connect_and_migrate(data_dir)
.await
.unwrap();
filamento = filamento::Client::new(jid.clone(), creds.password.to_string(), db);
} else {
let mut data_dir = dirs::data_dir()
.expect("operating system does not support retreiving determining default data dir");
data_dir.push("macaw");
data_dir.push(creds.jid.clone());
data_dir.push(creds.jid.clone());
// TODO: better lol
data_dir.set_extension("db");
info!("db_path: {:?}", data_dir);
let db = filamento::db::Db::create_connect_and_migrate(data_dir)
.await
.unwrap();
filamento = filamento::Client::new(jid.clone(), creds.password.to_string(), db);
}
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, mpsc::Receiver<UpdateMessage>)> = None;
if let Some(creds) = creds {
let jid = creds.jid.parse::<JID>();
match jid {
Ok(jid) => {
let (handle, updates) = filamento(&jid, &creds, &cfg).await;
client = Some((jid, handle, updates));
}
Err(e) => client_creation_error = Some(Error::CredentialsLoad(e.into())),
}
}
if let Some((jid, luz_handle, update_recv)) = client {
let stream = ReceiverStream::new(update_recv);
let stream = stream.map(|message| Message::Luz(message));
let task = {
let luz_handle1 = luz_handle.clone();
let luz_handle2 = luz_handle.clone();
if cfg.auto_connect {
Task::batch(
[
Task::batch([
Task::perform(
async move { luz_handle1.get_roster().await },
|result| {
let roster = result.unwrap();
let mut macaw_roster = HashMap::new();
for contact in roster {
macaw_roster.insert(contact.user_jid.clone(), contact);
}
Message::Roster(macaw_roster)
},
),
Task::perform(
async move {
luz_handle2.get_chats_ordered_with_latest_messages().await
},
|chats| {
let chats = chats.unwrap();
info!("got chats: {:?}", chats);
Message::GotChats(chats)
},
),
])
.chain(Task::done(Message::Connect)),
Task::stream(stream),
],
)
} else {
Task::batch([
Task::perform(async move { luz_handle1.get_roster().await }, |result| {
let roster = result.unwrap();
let mut macaw_roster = HashMap::new();
for contact in roster {
macaw_roster.insert(contact.user_jid.clone(), contact);
}
Message::Roster(macaw_roster)
}),
Task::perform(
async move { luz_handle2.get_chats_ordered_with_latest_messages().await },
|chats| {
let chats = chats.unwrap();
info!("got chats: {:?}", chats);
Message::GotChats(chats)
},
),
Task::stream(stream),
])
}
};
iced::application("Macaw", Macaw::update, Macaw::view)
.subscription(subscription)
.theme(Macaw::theme)
.run_with(|| {
(
Macaw::new(
Some(Client {
client: luz_handle,
// TODO:
jid,
// TODO: store cached status
status: Presence {
timestamp: Utc::now(),
presence: PresenceType::Offline(Offline::default()),
},
connection_state: ConnectionState::Offline,
}),
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),
Roster(HashMap<JID, Contact>),
Connect,
Disconnect,
GotChats(Vec<(Chat, ChatMessage)>),
GotMessageHistory(Chat, IndexMap<Uuid, 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::Error(error) => {
tracing::error!("Luz error: {:?}", error);
Task::none()
}
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();
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.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 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_jid, (chat, old_message))) =
self.chats.shift_remove_entry(&to)
{
self.chats
.insert_before(0, chat_jid, (chat, Some(message.clone())));
if let Some(open_chat) = &mut self.open_chat {
if open_chat.jid == to {
open_chat.update(message_view::Message::Message(message));
}
}
} else {
let chat = Chat {
correspondent: to.clone(),
};
let message_history = indexmap! {message.id => message.clone()};
self.chats.insert_before(0, to, (chat, Some(message)));
}
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();
if self.config.auto_connect {
Task::batch([
Task::perform(async move { client1.client.get_roster().await }, |result| {
let roster = result.unwrap();
let mut macaw_roster = HashMap::new();
for contact in roster {
macaw_roster.insert(contact.user_jid.clone(), contact);
}
Message::Roster(macaw_roster)
}),
Task::perform(
async move {
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().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_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::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;
})
.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.jid == jid {
self.open_chat = None;
return Task::none();
}
}
None => {}
}
self.open_chat = Some(MessageView::new(jid.clone(), &self.config));
let jid1 = jid.clone();
match &self.client {
Account::LoggedIn(client) => {
let client = client.clone();
Task::perform(
async move { client.get_messages(jid1).await },
move |result| match result {
Ok(h) => {
Message::MessageView(message_view::Message::MessageHistory(h))
}
Err(e) => Message::Error(Error::MessageHistory(jid.clone(), e)),
},
)
}
Account::LoggedOut(login_modal) => 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) = filamento(&jid, &creds, &config).await;
(handle, recv, jid, creds, config)
}, move |(handle, recv, jid, creds, config)| {
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,
},
)));
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 in chats {
self.chats
// TODO: could have a chat with no messages, bad database state
.insert(chat.0.correspondent.clone(), (chat.0.clone(), Some(chat.1)));
// let client = client.clone();
// let correspondent = chat.correspondent.clone();
// tasks.push(Task::perform(
// // TODO: don't get the entire message history LOL
// async move { (chat, client.get_messages(correspondent).await) },
// |result| {
// let messages: IndexMap<Uuid, ChatMessage> = result
// .1
// .unwrap()
// .into_iter()
// .map(|message| (message.id.clone(), message))
// .collect();
// Message::GotMessageHistory(result.0, messages)
// },
// ))
}
Task::batch(tasks)
// .then(|chats| {
// let tasks = Vec::new();
// for key in chats.keys() {
// let client = client.client.clone();
// tasks.push(Task::future(async {
// client.get_messages(key.clone()).await;
// }));
// }
// Task::batch(tasks)
// }),
}
Message::GotMessageHistory(chat, mut message_history) => {
// TODO: don't get the entire message history LOL
if let Some((_id, message)) = message_history.pop() {
self.chats
.insert(chat.correspondent.clone(), (chat, Some(message)));
}
Task::none()
}
Message::SendMessage(jid, body) => {
let client = match &self.client {
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.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![];
for (jid, (chat, latest_message)) in &self.chats {
let mut open = false;
if let Some(open_chat) = &self.open_chat {
if open_chat.jid == *jid {
open = true;
}
}
let chat_list_item = chat_list_item(chat, latest_message, open);
chats_list = chats_list.push(chat_list_item);
}
let chats_list = scrollable(chats_list.spacing(8).padding(8))
.spacing(1)
.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>(
chat: &'a Chat,
latest_message: &'a Option<ChatMessage>,
open: bool,
) -> Element<'a, Message> {
let mut content: Column<Message> = column![text(chat.correspondent.to_string())];
if let Some(latest_message) = latest_message {
let message = latest_message.body.body.replace("\n", " ");
let date = latest_message.timestamp.naive_local();
let now = Local::now().naive_local();
let timeinfo;
if date.date() == now.date() {
// TODO: localisation/config
timeinfo = text(date.time().format("%H:%M").to_string())
} else {
timeinfo = text(date.date().format("%d/%m").to_string())
}
content = content.push(
row![
container(text(message).wrapping(Wrapping::None))
.clip(true)
.width(Fill),
timeinfo
]
.spacing(8)
.width(Fill),
);
}
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()
}