implement log-in modal

This commit is contained in:
cel 🌸 2025-02-24 15:06:58 +00:00
parent 0ae95d1e7d
commit 5a2fae397c
4 changed files with 502 additions and 94 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/target
.vscode
macaw.db

115
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"
@ -1980,6 +2019,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"
@ -2254,6 +2303,7 @@ dependencies = [
"iced",
"jid",
"luz",
"secret-service",
"tokio",
"tokio-stream",
"tracing",
@ -2475,6 +2525,30 @@ dependencies = [
"winapi",
]
[[package]]
name = "num"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
dependencies = [
"num-bigint",
"num-complex",
"num-integer",
"num-iter",
"num-rational",
"num-traits",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.4"
@ -2492,6 +2566,15 @@ dependencies = [
"zeroize",
]
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.46"
@ -2512,6 +2595,17 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -3491,6 +3585,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"
@ -4246,6 +4359,7 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
"tracing",
"windows-sys 0.52.0",
]
@ -5454,6 +5568,7 @@ dependencies = [
"serde_repr",
"sha1",
"static_assertions",
"tokio",
"tracing",
"uds_windows",
"windows-sys 0.52.0",

View File

@ -11,4 +11,4 @@ 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"] }

View File

@ -1,38 +1,127 @@
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, column, row, text, text_input};
use iced::{stream, Element, Subscription, Task, Theme};
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::oneshot;
use tokio::sync::{mpsc, oneshot};
use tokio_stream::wrappers::ReceiverStream;
use tracing::info;
#[derive(Default)]
pub struct Macaw {
client: Option<LuzHandle>,
client: Account,
roster: HashMap<JID, Contact>,
users: HashMap<JID, User>,
presences: HashMap<JID, Presence>,
chats: HashMap<JID, (Chat, Vec<ChatMessage>)>,
subscription_requests: HashSet<JID>,
connection_status: Option<Presence>,
}
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();
iced::application("Macaw", Macaw::update, Macaw::view)
.subscription(Macaw::subscription)
.run()
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 {
ClientCreated(LuzHandle),
LoginModal(LoginModalMessage),
ClientCreated(Client),
Luz(UpdateMessage),
Roster(HashMap<JID, Contact>),
Connect,
@ -40,27 +129,15 @@ enum Message {
OpenChat(JID),
}
#[derive(Debug, Clone)]
enum LoginModalMessage {
JID(String),
Password(String),
Submit,
Error(Error),
}
impl Macaw {
fn stream() -> impl Stream<Item = Message> {
stream::channel(100, |mut output| async {
let (luz, recv) = LuzHandle::new(
"test@blos.sm".try_into().unwrap(),
"slayed".to_string(),
"./macaw.db",
)
.await
.unwrap();
output.send(Message::ClientCreated(luz)).await;
let stream = ReceiverStream::new(recv);
let stream = stream.map(|message| Message::Luz(message)).map(Ok);
stream.forward(output).await;
})
}
fn subscription(&self) -> Subscription<Message> {
Subscription::run(Macaw::stream)
}
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::Luz(update_message) => match update_message {
@ -68,18 +145,35 @@ impl Macaw {
tracing::error!("Luz error: {:?}", error);
Task::none()
}
UpdateMessage::Online(online, vec) => {
self.connection_status = Some(Presence::Online(online));
let mut roster = HashMap::new();
for contact in vec {
roster.insert(contact.user_jid.clone(), contact);
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()
}
self.roster = roster;
Task::none()
}
Account::LoggedOut {
jid,
password,
error,
} => Task::none(),
},
UpdateMessage::Offline(offline) => {
self.connection_status = Some(Presence::Offline(offline));
Task::none()
// 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();
@ -118,13 +212,13 @@ impl Macaw {
Task::none()
}
},
Message::ClientCreated(luz_handle) => {
let cloned: LuzHandle = luz_handle.clone();
self.client = Some(cloned);
// TODO: NEXT
Message::ClientCreated(client) => {
self.client = Account::LoggedIn(client.clone());
let (send, recv) = oneshot::channel();
Task::perform(
async move {
luz_handle.send(CommandMessage::GetRoster(send)).await;
client.client.send(CommandMessage::GetRoster(send)).await;
recv.await
},
|result| {
@ -141,64 +235,262 @@ impl Macaw {
self.roster = hash_map;
Task::none()
}
Message::Connect => {
let client = self.client.clone();
Task::future(async move {
client.clone().unwrap().send(CommandMessage::Connect).await;
})
.discard()
}
Message::Disconnect => {
let client = self.client.clone();
Task::future(async move {
client
.clone()
.unwrap()
.send(CommandMessage::Disconnect(Offline::default()))
.await;
})
.discard()
}
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 mut contacts: Vec<Element<Message>> = Vec::new();
for (_, contact) in &self.roster {
contacts.push(
button(match &contact.user_jid.localpart {
Some(u) => u,
None => "no username",
})
.on_press(Message::OpenChat(contact.user_jid.clone()))
.into(),
);
}
let column = column(contacts);
let connection_status = match &self.connection_status {
Some(s) => match s {
Presence::Online(online) => "connected",
Presence::Offline(offline) => "disconnected",
},
None => "no account",
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,
]
};
column![
row![
text("test@blos.sm"),
text(connection_status),
button("connect").on_press(Message::Connect),
button("disconnect").on_press(Message::Disconnect)
],
text("Buddy List:"),
//
//
column,
]
.into()
// 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()
}