mirror of
https://gitea.mizah.xyz/mizah/dergchat
synced 2024-11-21 13:55:00 -05:00
Basic login screen working.
This commit is contained in:
parent
99120cd0c8
commit
18b64ef030
386
src/main.rs
386
src/main.rs
@ -1,15 +1,18 @@
|
|||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
use std::collections::HashMap;
|
mod types;
|
||||||
use std::future::Future;
|
mod widgets;
|
||||||
use std::str::FromStr;
|
mod xmpp_interface;
|
||||||
use futures_util::stream::StreamExt;
|
|
||||||
|
use crate::dioxus_elements::div;
|
||||||
|
use dioxus::html::button;
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus_desktop::Config;
|
use dioxus_desktop::Config;
|
||||||
use log::info;
|
use futures_util::stream::StreamExt;
|
||||||
use tokio::select;
|
use jid::BareJid;
|
||||||
use xmpp::{BareJid, ClientBuilder, ClientType};
|
use std::future::Future;
|
||||||
use xmpp::parsers::message::MessageType;
|
use std::str::FromStr;
|
||||||
|
use widgets::App;
|
||||||
|
|
||||||
const STYLESHEET: &str = r#"
|
const STYLESHEET: &str = r#"
|
||||||
body {
|
body {
|
||||||
@ -28,351 +31,42 @@ const STYLESHEET: &str = r#"
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
margin: auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form input[type=text], .login-form input[type=password] {
|
||||||
|
display: block;
|
||||||
|
padding: 2mm;
|
||||||
|
margin: 2mm 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form input[type=checkbox] {
|
||||||
|
vertical-align: middle;
|
||||||
|
padding: 2mm;
|
||||||
|
margin: 2mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form button {
|
||||||
|
display: block;
|
||||||
|
padding: 2mm;
|
||||||
|
margin: 2mm 0;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
let entry = keyring::Entry::new("dergchat", "dergchat").expect("Failed to create keyring entry");
|
|
||||||
|
|
||||||
entry.set_password("topS3cr3tP4$$w0rd").expect("Failed to set password");
|
|
||||||
let password = entry.get_password().expect("Failed to get password");
|
|
||||||
println!("My password is '{}'", password);
|
|
||||||
entry.delete_password().expect("Failed to delete password");
|
|
||||||
|
|
||||||
hot_reload_init!();
|
hot_reload_init!();
|
||||||
|
|
||||||
// launch the dioxus app in a webview
|
// launch the dioxus app in a webview
|
||||||
dioxus_desktop::launch_cfg(App,
|
dioxus_desktop::launch_cfg(
|
||||||
Config::default()
|
App,
|
||||||
.with_custom_head(format!(r#"<style>{}</style>"#, STYLESHEET))
|
Config::default()
|
||||||
.with_background_color((32, 64, 32, 255)));
|
.with_custom_head(format!(r#"<style>{}</style>"#, STYLESHEET))
|
||||||
}
|
.with_background_color((32, 64, 32, 255)),
|
||||||
|
|
||||||
#[derive(Props)]
|
|
||||||
struct RoomListProps<'a> {
|
|
||||||
rooms: Vec<BareJid>,
|
|
||||||
on_room_picked: EventHandler<'a, BareJid>,
|
|
||||||
on_room_left: EventHandler<'a, BareJid>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A Dioxus component that renders a list of rooms
|
|
||||||
fn RoomList<'a>(cx: Scope<'a, RoomListProps>) -> Element<'a> {
|
|
||||||
render! {
|
|
||||||
ul {
|
|
||||||
list_style: "none",
|
|
||||||
flex_grow: 1,
|
|
||||||
margin: 0,
|
|
||||||
padding: 0,
|
|
||||||
for room in cx.props.rooms.iter() {
|
|
||||||
rsx! { li {
|
|
||||||
display: "flex",
|
|
||||||
flex_direction: "row",
|
|
||||||
|
|
||||||
onclick: |evt| cx.props.on_room_picked.call(room.to_owned()),
|
|
||||||
|
|
||||||
div {
|
|
||||||
flex_grow: 1,
|
|
||||||
"{room}"
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
|
|
||||||
onclick: |evt| {
|
|
||||||
evt.stop_propagation();
|
|
||||||
cx.props.on_room_left.call(room.to_owned());
|
|
||||||
},
|
|
||||||
"X"
|
|
||||||
}
|
|
||||||
} }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct Message {
|
|
||||||
sender: String,
|
|
||||||
body: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Props)]
|
|
||||||
struct RoomViewProps<'a> {
|
|
||||||
room: BareJid,
|
|
||||||
messages: Vec<Message>,
|
|
||||||
on_message_sent: EventHandler<'a, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Props)]
|
|
||||||
struct SendMessageProps<'a> {
|
|
||||||
on_message_sent: EventHandler<'a, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn RoomView<'a>(cx: Scope<'a, RoomViewProps>) -> Element<'a> {
|
|
||||||
|
|
||||||
let message = use_state(cx, || "".to_owned());
|
|
||||||
|
|
||||||
render! {
|
|
||||||
div {
|
|
||||||
padding: "5mm",
|
|
||||||
flex_grow: 1,
|
|
||||||
display: "flex",
|
|
||||||
background_color: "#166322",
|
|
||||||
flex_direction: "column",
|
|
||||||
h2 {
|
|
||||||
margin: 0,
|
|
||||||
padding: 0,
|
|
||||||
border_bottom: "1px solid lightgray",
|
|
||||||
"{cx.props.room}"
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
flex_grow: 1,
|
|
||||||
overflow_y: "scroll",
|
|
||||||
for message in cx.props.messages.iter() {
|
|
||||||
rsx! { li {
|
|
||||||
"{message.sender}: {message.body}"
|
|
||||||
} }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SendMessage {
|
|
||||||
on_message_sent: |x:String| cx.props.on_message_sent.call(x),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn SendMessage<'a>(cx: Scope<'a, SendMessageProps>) -> Element<'a> {
|
|
||||||
|
|
||||||
let message = use_state(cx, || "".to_owned());
|
|
||||||
|
|
||||||
render! {
|
|
||||||
div {
|
|
||||||
display: "flex",
|
|
||||||
flex_direction: "row",
|
|
||||||
textarea {
|
|
||||||
resize: "none",
|
|
||||||
flex_grow: 1,
|
|
||||||
oninput: |evt| {
|
|
||||||
message.set(evt.value.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
height: "100%",
|
|
||||||
onclick: move |_| {
|
|
||||||
cx.props.on_message_sent.call(message.current().to_string());
|
|
||||||
},
|
|
||||||
"Send"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_event(event: xmpp::Event,
|
|
||||||
agent: &mut xmpp::Agent,
|
|
||||||
messages: &mut UseRef<HashMap<BareJid, Vec<Message>>>) {
|
|
||||||
match event {
|
|
||||||
xmpp::Event::JoinRoom(jid, conference) => {
|
|
||||||
println!("Will auto-join room: {}", &jid);
|
|
||||||
agent.join_room(jid, None, None, "en/us", "online").await;
|
|
||||||
}
|
|
||||||
xmpp::Event::RoomJoined(room_jid) => {
|
|
||||||
println!("Joined room: {}", &room_jid);
|
|
||||||
messages.with_mut(move |m| { m.entry(room_jid.clone()).or_insert(vec![]); });
|
|
||||||
}
|
|
||||||
xmpp::Event::RoomLeft(room_jid) => {
|
|
||||||
println!("Left room: {}", &room_jid);
|
|
||||||
messages.with_mut(move |m| { m.remove(&room_jid); });
|
|
||||||
}
|
|
||||||
xmpp::Event::ChatMessage(id, sender, body) => {
|
|
||||||
println!("Message from {}: {}", &sender, &body.0);
|
|
||||||
messages.with_mut(move |m| {
|
|
||||||
m.entry(sender.clone()).or_insert(vec![]).push(Message {
|
|
||||||
sender: sender.to_string(),
|
|
||||||
body: body.0,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
xmpp::Event::RoomMessage(id, room_jid, sender_nick, body) => {
|
|
||||||
println!("Message in {} from {}: {}", &room_jid, &sender_nick, &body.0);
|
|
||||||
messages.with_mut(move |m| m.entry(room_jid.clone()).or_insert(vec![]).push(Message {
|
|
||||||
sender: sender_nick,
|
|
||||||
body: body.0,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
xmpp::Event::RoomPrivateMessage(id, room_jid, sender_nick, body) => {
|
|
||||||
println!("Private message in {} from {}: {}", &room_jid, &sender_nick, &body.0);
|
|
||||||
messages.with_mut(move |m| m.entry(room_jid.clone()).or_insert(vec![]).push(Message {
|
|
||||||
sender: sender_nick,
|
|
||||||
body: body.0,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
log::debug!("Received unsupported event {:?}", event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum NetworkCommand {
|
|
||||||
JoinRoom { room: BareJid },
|
|
||||||
LeaveRoom { room: BareJid },
|
|
||||||
SendMessage { recipient: BareJid, message: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Props)]
|
|
||||||
struct RoomJoinProps<'a> {
|
|
||||||
on_join_room: EventHandler<'a, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn RoomJoinWidget<'a>(cx: Scope<'a, RoomJoinProps>) -> Element<'a> {
|
|
||||||
|
|
||||||
let room = use_state(cx, || "".to_owned());
|
|
||||||
|
|
||||||
render! {
|
|
||||||
div {
|
|
||||||
display: "flex",
|
|
||||||
flex_direction: "row",
|
|
||||||
input {
|
|
||||||
flex_grow: 1,
|
|
||||||
display: "block",
|
|
||||||
oninput: |evt| {
|
|
||||||
room.set(evt.value.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
onclick: move |_| cx.props.on_join_room.call(room.current().to_string()),
|
|
||||||
"Join"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// define a component that renders a div with the text "Hello, world!"
|
|
||||||
fn App(cx: Scope) -> Element {
|
|
||||||
|
|
||||||
let messages = use_ref(cx, || HashMap::new());
|
|
||||||
let current_room = use_state(cx, || None::<BareJid>);
|
|
||||||
|
|
||||||
let cr = use_coroutine(cx,
|
|
||||||
|rx: UnboundedReceiver<NetworkCommand>| run_xmpp(messages.clone(), rx),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
render! {
|
|
||||||
div {
|
|
||||||
margin: 0,
|
|
||||||
padding: 0,
|
|
||||||
display: "flex",
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
|
|
||||||
div {
|
|
||||||
padding: "5mm",
|
|
||||||
background_color: "#1d852d",
|
|
||||||
display: "flex",
|
|
||||||
flex_direction: "column",
|
|
||||||
div {
|
|
||||||
border_bottom: "1px solid lightgray",
|
|
||||||
"Mizah"
|
|
||||||
}
|
|
||||||
RoomList {
|
|
||||||
rooms: messages.read().keys().cloned().collect(),
|
|
||||||
on_room_picked: move |x:BareJid| {
|
|
||||||
println!("Room selected: {:?}", x);
|
|
||||||
current_room.set(Some(x));
|
|
||||||
},
|
|
||||||
on_room_left: move |x:BareJid| {
|
|
||||||
println!("Leaving room: {:?}", x);
|
|
||||||
cr.send(NetworkCommand::LeaveRoom { room: x });
|
|
||||||
},
|
|
||||||
}
|
|
||||||
RoomJoinWidget {
|
|
||||||
on_join_room: move |x:String| {
|
|
||||||
println!("Joining room: {:?}", x);
|
|
||||||
cr.send(NetworkCommand::JoinRoom { room: BareJid::from_str(&x).unwrap() });
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(room) = current_room.get() {
|
|
||||||
let messages = messages.read().get(&room).expect("Selected non-existant room").to_vec();
|
|
||||||
|
|
||||||
rsx! {
|
|
||||||
RoomView {
|
|
||||||
room: room.clone(),
|
|
||||||
messages: messages,
|
|
||||||
on_message_sent: move |x:String| {
|
|
||||||
println!("Message sent: {:?}", x);
|
|
||||||
cr.send(NetworkCommand::SendMessage { recipient: room.clone(), message: x });
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
rsx! {
|
|
||||||
div {
|
|
||||||
padding: "5mm",
|
|
||||||
flex_grow: 1,
|
|
||||||
display: "flex",
|
|
||||||
background_color: "#166322",
|
|
||||||
"No room selected. Pick one from the list on the left."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_xmpp(mut room_data: UseRef<HashMap<BareJid, Vec<Message>>>,
|
|
||||||
mut commands: UnboundedReceiver<NetworkCommand>) -> impl Future<Output=()> + Sized {
|
|
||||||
|
|
||||||
async move {
|
|
||||||
|
|
||||||
let jid = BareJid::from_str("bot@mizah.xyz").unwrap();
|
|
||||||
|
|
||||||
let password = "TLOwnPNPDGN9nfRRqPwh";
|
|
||||||
|
|
||||||
let mut agent = ClientBuilder::new(jid, &password)
|
|
||||||
.set_client(ClientType::Pc, "dergchat")
|
|
||||||
.set_default_nick("dergchat")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
select! {
|
|
||||||
events = agent.wait_for_events() => {
|
|
||||||
if let Some(events) = events {
|
|
||||||
for event in events {
|
|
||||||
handle_event(event, &mut agent, &mut room_data).await;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
info!("Disconnected");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
command = commands.next() => {
|
|
||||||
if let Some(command) = command {
|
|
||||||
match command {
|
|
||||||
NetworkCommand::JoinRoom { room } => {
|
|
||||||
agent.join_room(room.clone(), None, None, "en-us", "online").await;
|
|
||||||
},
|
|
||||||
NetworkCommand::LeaveRoom { room } => {
|
|
||||||
agent.leave_room(
|
|
||||||
room,
|
|
||||||
"dergchat".to_string(),
|
|
||||||
"en-us",
|
|
||||||
"User left the room.").await;
|
|
||||||
},
|
|
||||||
NetworkCommand::SendMessage { recipient, message } => {
|
|
||||||
agent.send_message(recipient.into(), MessageType::Groupchat, "en-us", &message).await;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
info!("Command channel closed");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
29
src/types.rs
Normal file
29
src/types.rs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
use jid::BareJid;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Room {
|
||||||
|
pub messages: Vec<Message>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Message {
|
||||||
|
pub sender: String,
|
||||||
|
pub body: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LoginCredentials {
|
||||||
|
pub username: BareJid,
|
||||||
|
pub default_nick: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for LoginCredentials {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("LoginCredentials")
|
||||||
|
.field("username", &self.username)
|
||||||
|
.field("default_nick", &self.default_nick)
|
||||||
|
.field("password", &"********")
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
136
src/widgets/login_screen.rs
Normal file
136
src/widgets/login_screen.rs
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
use crate::types::LoginCredentials;
|
||||||
|
use dioxus::core::{Element, Scope};
|
||||||
|
use dioxus::hooks::use_state;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_elements::div;
|
||||||
|
use jid::BareJid;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum LoginStatus {
|
||||||
|
LoggedOut,
|
||||||
|
LoggingIn,
|
||||||
|
LoggedIn,
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn login_credentials_from_strings(
|
||||||
|
// username: String,
|
||||||
|
// default_nick: String,
|
||||||
|
// password: String,
|
||||||
|
// ) -> Result<LoginCredentials, String> {
|
||||||
|
// let username = BareJid::from_str(&username).map_err(|_| "Invalid username".to_string())?;
|
||||||
|
// let password = password;
|
||||||
|
//
|
||||||
|
// // Default nick must be non-empty
|
||||||
|
// let default_nick = if default_nick.is_empty() {
|
||||||
|
// Err("Default nick must be non-empty".to_string())
|
||||||
|
// } else {
|
||||||
|
// Ok(default_nick)
|
||||||
|
// }?;
|
||||||
|
//
|
||||||
|
// Ok(LoginCredentials {
|
||||||
|
// username,
|
||||||
|
// password,
|
||||||
|
// default_nick,
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
pub struct LoginAttempt {
|
||||||
|
pub username: String,
|
||||||
|
pub default_nick: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for LoginAttempt {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("LoginAttempt")
|
||||||
|
.field("username", &self.username)
|
||||||
|
.field("default_nick", &self.default_nick)
|
||||||
|
.field("password", &"********")
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Props)]
|
||||||
|
pub struct LoginScreenProps<'a> {
|
||||||
|
login_state: LoginStatus,
|
||||||
|
on_login_attempt: EventHandler<'a, LoginAttempt>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn LoginScreen<'a>(cx: Scope<'a, LoginScreenProps>) -> Element<'a> {
|
||||||
|
let username = use_state(cx, || "".to_string());
|
||||||
|
let default_nick = use_state(cx, || "".to_string());
|
||||||
|
let password = use_state(cx, || "".to_string());
|
||||||
|
let default_nick_was_changed = use_state(cx, || false);
|
||||||
|
|
||||||
|
render! {
|
||||||
|
|
||||||
|
div {
|
||||||
|
class: "login-form",
|
||||||
|
input {
|
||||||
|
"type": "text",
|
||||||
|
placeholder: "Username",
|
||||||
|
oninput: move |x| {
|
||||||
|
username.set(x.value.clone());
|
||||||
|
|
||||||
|
if !default_nick_was_changed.get() {
|
||||||
|
if let Some(before_at) = x.value.split('@').next() {
|
||||||
|
default_nick.set(before_at.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
placeholder: "Default nick",
|
||||||
|
"type": "text",
|
||||||
|
oninput: move |x| {
|
||||||
|
default_nick.set(x.value.clone());
|
||||||
|
default_nick_was_changed.set(true);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
placeholder: "Password",
|
||||||
|
"type": "password",
|
||||||
|
oninput: move |x| {
|
||||||
|
password.set(x.value.clone());
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if let LoginStatus::Error(e) = &cx.props.login_state {
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
class: "error",
|
||||||
|
"{e}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
button {
|
||||||
|
|
||||||
|
class: match cx.props.login_state {
|
||||||
|
LoginStatus::LoggedOut => "login-button logged-out",
|
||||||
|
LoginStatus::LoggingIn => "login-button logging-in",
|
||||||
|
LoginStatus::LoggedIn => "login-button logged-in",
|
||||||
|
LoginStatus::Error(_) => "error",
|
||||||
|
},
|
||||||
|
|
||||||
|
onclick: move |_| {
|
||||||
|
cx.props.on_login_attempt.call(LoginAttempt {
|
||||||
|
username: username.current().to_string(),
|
||||||
|
default_nick: default_nick.current().to_string(),
|
||||||
|
password: password.current().to_string(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
"Login"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
181
src/widgets/mod.rs
Normal file
181
src/widgets/mod.rs
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
use crate::types::{LoginCredentials, Message};
|
||||||
|
use crate::widgets::login_screen::LoginAttempt;
|
||||||
|
use crate::widgets::login_screen::{LoginScreen, LoginStatus};
|
||||||
|
use crate::widgets::room_join_widget::{RoomJoinProps, RoomJoinWidget};
|
||||||
|
use crate::widgets::room_list::{RoomList, RoomListProps};
|
||||||
|
use crate::widgets::room_view::RoomView;
|
||||||
|
use crate::xmpp_interface::xmpp_mainloop;
|
||||||
|
use crate::xmpp_interface::NetworkCommand;
|
||||||
|
use dioxus::core::{Element, Scope};
|
||||||
|
use dioxus::core_macro::Props;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use futures_util::StreamExt;
|
||||||
|
use jid::BareJid;
|
||||||
|
use log::{error, info};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::format;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use xmpp::{Agent, ClientBuilder, ClientType};
|
||||||
|
|
||||||
|
mod login_screen;
|
||||||
|
mod room_join_widget;
|
||||||
|
mod room_list;
|
||||||
|
mod room_view;
|
||||||
|
mod send_message;
|
||||||
|
|
||||||
|
async fn await_online(agent: &mut Agent) -> Result<(), String> {
|
||||||
|
let events = agent
|
||||||
|
.wait_for_events()
|
||||||
|
.await
|
||||||
|
.ok_or("Stream closed unexpectedly".to_string())?;
|
||||||
|
|
||||||
|
assert_eq!(events.len(), 1);
|
||||||
|
|
||||||
|
match events.into_iter().next().expect("No events") {
|
||||||
|
xmpp::Event::Online => {
|
||||||
|
info!("Online");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
xmpp::Event::Disconnected => {
|
||||||
|
error!("Disconnected");
|
||||||
|
Err("Disconnected".to_string())
|
||||||
|
}
|
||||||
|
e => return Err(format!("Unexpected event: {:?}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub async fn run_xmpp_toplevel(
|
||||||
|
connection_status: UseState<LoginStatus>,
|
||||||
|
mut messages: UseRef<HashMap<BareJid, Vec<Message>>>,
|
||||||
|
mut commands: UnboundedReceiver<NetworkCommand>,
|
||||||
|
) {
|
||||||
|
|
||||||
|
// Await a login attempt:
|
||||||
|
|
||||||
|
let cmd = commands.next().await;
|
||||||
|
|
||||||
|
if let Some(NetworkCommand::TryLogin { credentials }) = cmd {
|
||||||
|
println!("Received credentials: {:?}", credentials);
|
||||||
|
|
||||||
|
connection_status.set(LoginStatus::LoggingIn);
|
||||||
|
|
||||||
|
let mut agent = ClientBuilder::new(credentials.username, &*credentials.password)
|
||||||
|
.set_client(ClientType::Pc, "dergchat")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
match await_online(&mut agent).await {
|
||||||
|
Ok(_) => {
|
||||||
|
connection_status.set(LoginStatus::LoggedIn);
|
||||||
|
info!("Connected");
|
||||||
|
|
||||||
|
xmpp_mainloop(&mut agent, &mut messages, &mut commands).await;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to connect: {}", e);
|
||||||
|
connection_status.set(LoginStatus::Error("Failed to connect".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Expected TryLogin command");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn App(cx: Scope) -> Element {
|
||||||
|
let messages = use_ref(cx, || HashMap::new());
|
||||||
|
let current_room = use_state(cx, || None::<BareJid>);
|
||||||
|
let connection_status = use_state(cx, || LoginStatus::LoggedOut);
|
||||||
|
|
||||||
|
let coroutine = use_coroutine(cx, |rx: UnboundedReceiver<NetworkCommand>|
|
||||||
|
run_xmpp_toplevel(connection_status.to_owned(), messages.to_owned(), rx));
|
||||||
|
|
||||||
|
render! {
|
||||||
|
|
||||||
|
// If not logged in, show a login screen.
|
||||||
|
if connection_status.get() != &LoginStatus::LoggedIn {
|
||||||
|
rsx! {
|
||||||
|
LoginScreen {
|
||||||
|
login_state: connection_status.current().as_ref().clone(),
|
||||||
|
on_login_attempt: move |x: LoginAttempt| {
|
||||||
|
coroutine.send(NetworkCommand::TryLogin {
|
||||||
|
credentials: LoginCredentials {
|
||||||
|
username: BareJid::from_str(&x.username).expect("Invalid JID"),
|
||||||
|
default_nick: x.default_nick,
|
||||||
|
password: x.password,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// We're logged in; show a sidebar and a room view.
|
||||||
|
|
||||||
|
rsx!{
|
||||||
|
|
||||||
|
// Sidebar.
|
||||||
|
div {
|
||||||
|
padding: "5mm",
|
||||||
|
background_color: "#1d852d",
|
||||||
|
display: "flex",
|
||||||
|
flex_direction: "column",
|
||||||
|
|
||||||
|
div {
|
||||||
|
border_bottom: "1px solid lightgray",
|
||||||
|
"Mizah"
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomList {
|
||||||
|
rooms: messages.read().keys().cloned().collect(),
|
||||||
|
on_room_picked: move |x: BareJid| {
|
||||||
|
current_room.set(Some(x));
|
||||||
|
},
|
||||||
|
on_room_left: move |x: BareJid| {
|
||||||
|
coroutine.send(NetworkCommand::LeaveRoom { room: x });
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomJoinWidget {
|
||||||
|
on_join_room: move |x: String| {
|
||||||
|
coroutine.send(NetworkCommand::JoinRoom {
|
||||||
|
room: BareJid::from_str(&x).expect("Invalid JID"),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Room view.
|
||||||
|
|
||||||
|
if let Some(room) = current_room.get() {
|
||||||
|
let messages = messages.read().get(&room).expect("Selected non-existant room").to_vec();
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
RoomView {
|
||||||
|
room: room.clone(),
|
||||||
|
messages: messages,
|
||||||
|
on_message_sent: move |x:String| {
|
||||||
|
println!("Message sent: {:?}", x);
|
||||||
|
coroutine.send(NetworkCommand::SendMessage { recipient: room.clone(), message: x });
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No room selected
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
padding: "5mm",
|
||||||
|
flex_grow: 1,
|
||||||
|
display: "flex",
|
||||||
|
background_color: "#166322",
|
||||||
|
"No room selected. Pick one from the list on the left."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
31
src/widgets/room_join_widget.rs
Normal file
31
src/widgets/room_join_widget.rs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
use dioxus::core::{Element, Scope};
|
||||||
|
use dioxus::core_macro::Props;
|
||||||
|
use dioxus::hooks::use_state;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Props)]
|
||||||
|
pub struct RoomJoinProps<'a> {
|
||||||
|
on_join_room: EventHandler<'a, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn RoomJoinWidget<'a>(cx: Scope<'a, RoomJoinProps>) -> Element<'a> {
|
||||||
|
let room = use_state(cx, || "".to_owned());
|
||||||
|
|
||||||
|
render! {
|
||||||
|
div {
|
||||||
|
display: "flex",
|
||||||
|
flex_direction: "row",
|
||||||
|
input {
|
||||||
|
flex_grow: 1,
|
||||||
|
display: "block",
|
||||||
|
oninput: |evt| {
|
||||||
|
room.set(evt.value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
onclick: move |_| cx.props.on_join_room.call(room.current().to_string()),
|
||||||
|
"Join"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
src/widgets/room_list.rs
Normal file
45
src/widgets/room_list.rs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
use dioxus::core::{Element, Scope};
|
||||||
|
use dioxus::core_macro::Props;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use jid::BareJid;
|
||||||
|
|
||||||
|
#[derive(Props)]
|
||||||
|
pub struct RoomListProps<'a> {
|
||||||
|
rooms: Vec<BareJid>,
|
||||||
|
on_room_picked: EventHandler<'a, BareJid>,
|
||||||
|
on_room_left: EventHandler<'a, BareJid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Dioxus component that renders a list of rooms
|
||||||
|
pub fn RoomList<'a>(cx: Scope<'a, RoomListProps>) -> Element<'a> {
|
||||||
|
render! {
|
||||||
|
ul {
|
||||||
|
list_style: "none",
|
||||||
|
flex_grow: 1,
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
for room in cx.props.rooms.iter() {
|
||||||
|
rsx! { li {
|
||||||
|
display: "flex",
|
||||||
|
flex_direction: "row",
|
||||||
|
|
||||||
|
onclick: |evt| cx.props.on_room_picked.call(room.to_owned()),
|
||||||
|
|
||||||
|
div {
|
||||||
|
flex_grow: 1,
|
||||||
|
"{room}"
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
|
||||||
|
onclick: |evt| {
|
||||||
|
evt.stop_propagation();
|
||||||
|
cx.props.on_room_left.call(room.to_owned());
|
||||||
|
},
|
||||||
|
"X"
|
||||||
|
}
|
||||||
|
} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
src/widgets/room_view.rs
Normal file
46
src/widgets/room_view.rs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
use crate::types::Message;
|
||||||
|
use crate::widgets::send_message::SendMessage;
|
||||||
|
use dioxus::core::{Element, Scope};
|
||||||
|
use dioxus::core_macro::Props;
|
||||||
|
use dioxus::hooks::use_state;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use jid::BareJid;
|
||||||
|
|
||||||
|
#[derive(Props)]
|
||||||
|
pub struct RoomViewProps<'a> {
|
||||||
|
room: BareJid,
|
||||||
|
messages: Vec<Message>,
|
||||||
|
on_message_sent: EventHandler<'a, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn RoomView<'a>(cx: Scope<'a, RoomViewProps>) -> Element<'a> {
|
||||||
|
let message = use_state(cx, || "".to_owned());
|
||||||
|
|
||||||
|
render! {
|
||||||
|
div {
|
||||||
|
padding: "5mm",
|
||||||
|
flex_grow: 1,
|
||||||
|
display: "flex",
|
||||||
|
background_color: "#166322",
|
||||||
|
flex_direction: "column",
|
||||||
|
h2 {
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
border_bottom: "1px solid lightgray",
|
||||||
|
"{cx.props.room}"
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
flex_grow: 1,
|
||||||
|
overflow_y: "scroll",
|
||||||
|
for message in cx.props.messages.iter() {
|
||||||
|
rsx! { li {
|
||||||
|
"{message.sender}: {message.body}"
|
||||||
|
} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SendMessage {
|
||||||
|
on_message_sent: |x:String| cx.props.on_message_sent.call(x),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
src/widgets/send_message.rs
Normal file
34
src/widgets/send_message.rs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
use dioxus::core::{Element, Scope};
|
||||||
|
use dioxus::core_macro::Props;
|
||||||
|
use dioxus::hooks::use_state;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Props)]
|
||||||
|
pub struct SendMessageProps<'a> {
|
||||||
|
on_message_sent: EventHandler<'a, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn SendMessage<'a>(cx: Scope<'a, SendMessageProps>) -> Element<'a> {
|
||||||
|
let message = use_state(cx, || "".to_owned());
|
||||||
|
|
||||||
|
render! {
|
||||||
|
div {
|
||||||
|
display: "flex",
|
||||||
|
flex_direction: "row",
|
||||||
|
textarea {
|
||||||
|
resize: "none",
|
||||||
|
flex_grow: 1,
|
||||||
|
oninput: |evt| {
|
||||||
|
message.set(evt.value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
height: "100%",
|
||||||
|
onclick: move |_| {
|
||||||
|
cx.props.on_message_sent.call(message.current().to_string());
|
||||||
|
},
|
||||||
|
"Send"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
127
src/xmpp_interface.rs
Normal file
127
src/xmpp_interface.rs
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
use crate::types::{LoginCredentials, Message};
|
||||||
|
use dioxus::hooks::{UnboundedReceiver, UseRef};
|
||||||
|
use futures_util::stream::StreamExt;
|
||||||
|
use jid::BareJid;
|
||||||
|
use log::{error, info};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use tokio::select;
|
||||||
|
use xmpp::parsers::message::MessageType;
|
||||||
|
use xmpp::{Agent, ClientBuilder, ClientType};
|
||||||
|
|
||||||
|
async fn handle_event(
|
||||||
|
event: xmpp::Event,
|
||||||
|
agent: &mut xmpp::Agent,
|
||||||
|
messages: &mut UseRef<HashMap<BareJid, Vec<Message>>>,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
xmpp::Event::JoinRoom(jid, conference) => {
|
||||||
|
println!("Will auto-join room: {}", &jid);
|
||||||
|
agent.join_room(jid, None, None, "en/us", "online").await;
|
||||||
|
}
|
||||||
|
xmpp::Event::RoomJoined(room_jid) => {
|
||||||
|
println!("Joined room: {}", &room_jid);
|
||||||
|
messages.with_mut(move |m| {
|
||||||
|
m.entry(room_jid.clone()).or_insert(vec![]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
xmpp::Event::RoomLeft(room_jid) => {
|
||||||
|
println!("Left room: {}", &room_jid);
|
||||||
|
messages.with_mut(move |m| {
|
||||||
|
m.remove(&room_jid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
xmpp::Event::ChatMessage(id, sender, body) => {
|
||||||
|
println!("Message from {}: {}", &sender, &body.0);
|
||||||
|
messages.with_mut(move |m| {
|
||||||
|
m.entry(sender.clone()).or_insert(vec![]).push(Message {
|
||||||
|
sender: sender.to_string(),
|
||||||
|
body: body.0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
xmpp::Event::RoomMessage(id, room_jid, sender_nick, body) => {
|
||||||
|
println!(
|
||||||
|
"Message in {} from {}: {}",
|
||||||
|
&room_jid, &sender_nick, &body.0
|
||||||
|
);
|
||||||
|
messages.with_mut(move |m| {
|
||||||
|
m.entry(room_jid.clone()).or_insert(vec![]).push(Message {
|
||||||
|
sender: sender_nick,
|
||||||
|
body: body.0,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
xmpp::Event::RoomPrivateMessage(id, room_jid, sender_nick, body) => {
|
||||||
|
println!(
|
||||||
|
"Private message in {} from {}: {}",
|
||||||
|
&room_jid, &sender_nick, &body.0
|
||||||
|
);
|
||||||
|
messages.with_mut(move |m| {
|
||||||
|
m.entry(room_jid.clone()).or_insert(vec![]).push(Message {
|
||||||
|
sender: sender_nick,
|
||||||
|
body: body.0,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::debug!("Received unsupported event {:?}", event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum NetworkCommand {
|
||||||
|
TryLogin { credentials: LoginCredentials },
|
||||||
|
JoinRoom { room: BareJid },
|
||||||
|
LeaveRoom { room: BareJid },
|
||||||
|
SendMessage { recipient: BareJid, message: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn xmpp_mainloop<'a>(
|
||||||
|
agent: &'a mut Agent,
|
||||||
|
mut room_data: &'a mut UseRef<HashMap<BareJid, Vec<Message>>>,
|
||||||
|
commands: &'a mut UnboundedReceiver<NetworkCommand>,
|
||||||
|
) -> impl Future<Output = ()> + Sized + 'a {
|
||||||
|
async move {
|
||||||
|
loop {
|
||||||
|
select! {
|
||||||
|
events = agent.wait_for_events() => {
|
||||||
|
if let Some(events) = events {
|
||||||
|
for event in events {
|
||||||
|
handle_event(event, agent, &mut room_data).await;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info!("Disconnected");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
command = commands.next() => {
|
||||||
|
if let Some(command) = command {
|
||||||
|
match command {
|
||||||
|
NetworkCommand::JoinRoom { room } => {
|
||||||
|
agent.join_room(room.clone(), None, None, "en-us", "online").await;
|
||||||
|
},
|
||||||
|
NetworkCommand::LeaveRoom { room } => {
|
||||||
|
agent.leave_room(
|
||||||
|
room,
|
||||||
|
"dergchat".to_string(),
|
||||||
|
"en-us",
|
||||||
|
"User left the room.").await;
|
||||||
|
},
|
||||||
|
NetworkCommand::SendMessage { recipient, message } => {
|
||||||
|
agent.send_message(recipient.into(), MessageType::Groupchat, "en-us", &message).await;
|
||||||
|
},
|
||||||
|
NetworkCommand::TryLogin { credentials } => {
|
||||||
|
panic!("Already logged in.");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info!("Command channel closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user