Compare commits

..

No commits in common. "18e907386d629966236d0b6a30501f492133d2f6" and "50e84d47a458420c68ae94dfaa37d901d2a8a4f1" have entirely different histories.

4 changed files with 326 additions and 420 deletions

82
Cargo.lock generated
View File

@ -33,6 +33,17 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "ahash"
version = "0.7.8"
@ -371,6 +382,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block-padding"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
dependencies = [
"generic-array",
]
[[package]]
name = "block2"
version = "0.5.1"
@ -463,6 +483,15 @@ dependencies = [
"wayland-client",
]
[[package]]
name = "cbc"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
dependencies = [
"cipher",
]
[[package]]
name = "cc"
version = "1.2.13"
@ -498,6 +527,16 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "circular"
version = "0.3.0"
@ -2037,6 +2076,16 @@ dependencies = [
"hashbrown 0.15.2",
]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"block-padding",
"generic-array",
]
[[package]]
name = "instant"
version = "0.1.13"
@ -2339,11 +2388,9 @@ dependencies = [
"jid",
"keyring",
"luz",
"serde",
"thiserror 2.0.11",
"secret-service",
"tokio",
"tokio-stream",
"toml",
"tracing",
"tracing-subscriber",
"uuid",
@ -3631,6 +3678,25 @@ dependencies = [
"tiny-skia",
]
[[package]]
name = "secret-service"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4d35ad99a181be0a60ffcbe85d680d98f87bdc4d7644ade319b87076b9dbfd4"
dependencies = [
"aes",
"cbc",
"futures-util",
"generic-array",
"hkdf",
"num",
"once_cell",
"rand",
"serde",
"sha2",
"zbus",
]
[[package]]
name = "security-framework"
version = "2.11.1"
@ -3675,18 +3741,18 @@ checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe"
[[package]]
name = "serde"
version = "1.0.218"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.218"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
@ -4409,6 +4475,7 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
"tracing",
"windows-sys 0.52.0",
]
@ -5634,6 +5701,7 @@ dependencies = [
"serde_repr",
"sha1",
"static_assertions",
"tokio",
"tracing",
"uds_windows",
"windows-sys 0.52.0",

View File

@ -11,10 +11,8 @@ tokio = "1.43.0"
tokio-stream = "0.1.17"
tracing-subscriber = "0.3.19"
tracing = "0.1.41"
secret-service = { version = "4.0.0", features = ["rt-tokio-crypto-rust"] }
confy = "0.6.1"
keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service"] }
uuid = { version = "1.13.1", features = ["v4"] }
indexmap = "2.7.1"
serde = { version = "1.0.218", features = ["derive"] }
thiserror = "2.0.11"
toml = "0.8"

View File

@ -1,193 +0,0 @@
use iced::{
futures::StreamExt,
widget::{button, checkbox, column, container, text, text_input},
Element, Task,
};
use jid::JID;
use keyring::Entry;
use luz::{
presence::{Offline, Presence},
LuzHandle,
};
use serde::{Deserialize, Serialize};
use tokio_stream::wrappers::ReceiverStream;
use tracing::info;
use crate::Client;
#[derive(Default)]
pub struct LoginModal {
jid: String,
password: String,
remember_me: bool,
error: Option<Error>,
}
#[derive(Debug, Clone)]
pub enum Message {
JID(String),
Password(String),
RememberMe,
Submit,
Error(Error),
}
#[derive(Debug, Clone)]
pub enum Error {
InvalidJID(String),
DatabaseConnection,
}
pub enum Action {
None,
ClientCreated(Task<crate::Message>),
}
#[derive(Serialize, Deserialize)]
pub struct Creds {
pub jid: String,
pub password: String,
}
impl LoginModal {
pub fn update(&mut self, message: Message) -> Action {
match message {
Message::JID(j) => {
self.jid = j;
Action::None
}
Message::Password(p) => {
self.password = p;
Action::None
}
Message::RememberMe => {
self.remember_me = !self.remember_me;
Action::None
}
Message::Submit => {
info!("submitting login");
let jid_str = self.jid.clone();
let password = self.password.clone();
let remember_me = self.remember_me.clone();
Action::ClientCreated(
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 mut tasks = Vec::new();
tasks.push(Task::done(crate::Message::ClientCreated(
Client {
client: luz_handle,
jid: j,
connection_status: Presence::Offline(
Offline::default(),
),
},
)));
let stream = ReceiverStream::new(receiver);
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 = Creds {
jid: jid_str,
password,
};
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
}
Err(_e) => {
tracing::error!("error (database probably)");
return vec![Task::done(crate::Message::LoginModal(
Message::Error(Error::DatabaseConnection),
))];
}
}
}
Err(_) => {
tracing::error!("parsing jid");
return vec![Task::done(crate::Message::LoginModal(
Message::Error(Error::InvalidJID(jid_str.to_string())),
))];
}
}
})
.then(|tasks| Task::batch(tasks)),
)
}
Message::Error(error) => {
self.error = Some(error);
Action::None
}
}
}
pub fn view(&self) -> Element<Message> {
container(
column![
text("Log In").size(24),
column![
column![
text("JID").size(12),
text_input("berry@macaw.chat", &self.jid)
.on_input(|j| Message::JID(j))
.on_submit(Message::Submit)
.padding(5),
]
.spacing(5),
column![
text("Password").size(12),
text_input("", &self.password)
.on_input(|p| Message::Password(p))
.on_submit(Message::Submit)
.secure(true)
.padding(5),
]
.spacing(5),
checkbox("remember me", self.remember_me).on_toggle(|_| Message::RememberMe),
button(text("Submit")).on_press(Message::Submit),
]
.spacing(10)
]
.spacing(20),
)
.width(300)
.padding(10)
.style(container::rounded_box)
.into()
}
}

View File

@ -2,13 +2,12 @@ use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
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,
button, center, column, container, mouse_area, opaque, row, scrollable, stack, text,
text_input, Column, Text, Toggler,
};
use iced::Length::Fill;
@ -16,21 +15,15 @@ use iced::{stream, Color, Element, Subscription, Task, Theme};
use indexmap::{indexmap, IndexMap};
use jid::JID;
use keyring::Entry;
use login_modal::{Creds, 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 thiserror::Error;
use tokio::sync::{mpsc, oneshot};
use tokio_stream::wrappers::ReceiverStream;
use tracing::{error, info};
use tracing::info;
use uuid::Uuid;
mod login_modal;
#[derive(Serialize, Deserialize)]
pub struct Config {
auto_connect: bool,
}
@ -43,7 +36,6 @@ impl Default for Config {
pub struct Macaw {
client: Account,
config: Config,
roster: HashMap<JID, Contact>,
users: HashMap<JID, User>,
presences: HashMap<JID, Presence>,
@ -60,18 +52,26 @@ pub struct OpenChat {
pub struct NewChat;
pub struct Creds {
jid: String,
password: String,
}
impl Macaw {
pub fn new(client: Option<Client>, config: Config) -> Self {
pub fn new(client: Option<Client>) -> Self {
let account;
if let Some(client) = client {
account = Account::LoggedIn(client);
} else {
account = Account::LoggedOut(LoginModal::default());
account = Account::LoggedOut {
jid: "".to_string(),
password: "".to_string(),
error: None,
};
}
Self {
client: account,
config,
roster: HashMap::new(),
users: HashMap::new(),
presences: HashMap::new(),
@ -85,7 +85,17 @@ impl Macaw {
pub enum Account {
LoggedIn(Client),
LoggedOut(LoginModal),
LoggedOut {
jid: String,
password: String,
error: Option<Error>,
},
}
#[derive(Debug, Clone)]
pub enum Error {
InvalidJID(String),
DatabaseConnection,
}
#[derive(Clone, Debug)]
@ -109,100 +119,35 @@ impl Deref for Client {
}
}
#[tokio::main]
async fn main() -> iced::Result {
fn main() -> iced::Result {
tracing_subscriber::fmt::init();
let cfg: Config = confy::load("macaw", None).unwrap();
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, LuzHandle, mpsc::Receiver<UpdateMessage>)> = None;
if let Some(creds) = creds {
let jid = creds.jid.parse::<JID>();
match jid {
Ok(jid) => {
let luz = LuzHandle::new(jid.clone(), creds.password.to_string(), "macaw.db").await;
match luz {
Ok((handle, recv)) => client = Some((jid.as_bare(), handle, recv)),
Err(e) => client_creation_error = Some(Error::ClientCreation(e)),
}
}
Err(e) => client_creation_error = Some(Error::CredentialsLoad(e.into())),
}
}
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));
let task = {
if cfg.auto_connect {
Task::batch([Task::stream(stream), Task::done(Message::Connect)])
} else {
Task::stream(stream)
}
};
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,
),
Macaw::new(Some(Client {
client: luz_handle,
// TODO:
jid,
connection_status: Presence::Offline(Offline::default()),
})),
// TODO: autoconnect config
task,
Task::stream(stream),
)
})
} 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()))
}
iced::application("Macaw", Macaw::update, Macaw::view)
.run_with(|| (Macaw::new(None), Task::none()))
}
}
#[derive(Debug, Clone)]
pub enum Message {
LoginModal(login_modal::Message),
enum Message {
LoginModal(LoginModalMessage),
ClientCreated(Client),
Luz(UpdateMessage),
Roster(HashMap<JID, Contact>),
@ -214,49 +159,16 @@ pub enum Message {
CloseChat(JID),
MessageCompose(String),
SendMessage(JID, String),
}
#[derive(Debug, Clone)]
enum LoginModalMessage {
JID(String),
Password(String),
Submit,
Error(Error),
}
#[derive(Debug, Error, Clone)]
pub enum Error {
#[error("failed to create Luz client: {0}")]
ClientCreation(#[from] luz::error::DatabaseError),
#[error("failed to save credentials: {0}")]
CredentialsSave(CredentialsSaveError),
#[error("failed to load credentials: {0}")]
CredentialsLoad(CredentialsLoadError),
}
#[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 {
@ -275,7 +187,11 @@ impl Macaw {
self.roster = roster;
Task::none()
}
Account::LoggedOut(login_modal) => Task::none(),
Account::LoggedOut {
jid,
password,
error,
} => Task::none(),
},
UpdateMessage::Offline(offline) => {
// TODO: update all contacts' presences to unknown (offline)
@ -284,7 +200,11 @@ impl Macaw {
client.connection_status = Presence::Offline(offline);
Task::none()
}
Account::LoggedOut(login_modal) => Task::none(),
Account::LoggedOut {
jid,
password,
error,
} => Task::none(),
}
}
UpdateMessage::FullRoster(vec) => {
@ -329,46 +249,24 @@ impl Macaw {
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().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)
}),
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().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)
}),
])
}
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;
@ -382,7 +280,11 @@ impl Macaw {
})
.discard()
}
Account::LoggedOut(login_modal) => Task::none(),
Account::LoggedOut {
jid,
password,
error,
} => Task::none(),
},
Message::Disconnect => match &self.client {
Account::LoggedIn(client) => {
@ -394,7 +296,11 @@ impl Macaw {
})
.discard()
}
Account::LoggedOut(login_modal) => Task::none(),
Account::LoggedOut {
jid,
password,
error,
} => Task::none(),
},
Message::OpenChat(jid) => {
self.open_chat = Some(OpenChat {
@ -403,25 +309,105 @@ impl Macaw {
});
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::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()
}
},
},
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();
}
Account::LoggedOut {
jid,
password,
error,
} => panic!("no client"),
};
for chat in chats {
let client = client.clone();
@ -469,17 +455,17 @@ impl Macaw {
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();
}
Account::LoggedOut {
jid,
password,
error,
} => todo!(),
};
Task::future(
async move { client.send_message(jid, luz::chat::Body { body }).await },
)
.discard()
}
Message::Error(error) => todo!("error notification toasts, logging?"),
}
}
@ -512,11 +498,19 @@ impl Macaw {
Presence::Online(_online) => "online",
Presence::Offline(_offline) => "disconnected",
},
Account::LoggedOut(_) => "disconnected",
Account::LoggedOut {
jid: _,
password: _,
error,
} => "disconnected",
};
let client_jid: Cow<'_, str> = match &self.client {
Account::LoggedIn(client) => (&client.jid).into(),
Account::LoggedOut(_) => Cow::from("no account"),
Account::LoggedOut {
jid: _,
password: _,
error,
} => Cow::from("no account"),
// map(|client| (&client.jid).into());
};
let account_view = row![
@ -574,8 +568,7 @@ impl Macaw {
.into();
if let Some(new_chat) = &self.new_chat {
// TODO: close new chat window
ui = modal(ui, text("new chat"), None);
ui = modal(ui, text("new chat"));
}
// temporarily center to fill space
// let ui = center(ui).into();
@ -583,9 +576,47 @@ impl Macaw {
match &self.client {
Account::LoggedIn(_client) => ui.into(),
Account::LoggedOut(login_modal) => {
let signup = login_modal.view().map(Message::LoginModal);
modal(ui, signup, None)
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)
}
}
}
@ -598,25 +629,27 @@ impl Macaw {
fn modal<'a, Message>(
base: impl Into<Element<'a, Message>>,
content: impl Into<Element<'a, Message>>,
on_blur: Option<Message>,
// on_blur: 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
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()
}
.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()
})) // .on_press(on_blur)
)
]
.into()
}