diff --git a/src/configuration.rs b/src/configuration.rs new file mode 100644 index 0000000..57d4b06 --- /dev/null +++ b/src/configuration.rs @@ -0,0 +1,76 @@ +// Dergchat, a free XMPP client. +// Copyright (C) 2023 Werner Kroneman +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use dioxus::prelude::*; +use keyring::Entry; +use log::error; +use serde_derive::{Deserialize, Serialize}; +use crate::widgets::login_screen::LoginAttempt; + +/// The configuration struct containing all the configuration options. +#[derive(Serialize, Deserialize, Default)] +pub struct Configuration { + pub stored_username: String, + pub stored_default_nick: String, +} + +/// Cache the login attempt into the config and keyring. +/// +/// Specifically, we store the username and default nick in the config file, and the password in the keyring. +/// +/// Note: we do NOT store the password in the config file, because the latter is stored in plaintext. +/// +/// # Arguments +/// * `attempt` - The login attempt to store. +/// * `config` - The current app configuration; UseState should prevent race conditions in writing. +pub fn store_login_details(attempt: LoginAttempt, config: &mut UseRef) { + + // Extract the fields from the login attempt. + let LoginAttempt { + username, + default_nick, + password, + } = attempt; + + // Open the config state and store the username and default nick. + config.with_mut(|c| { + + // Open the keyring and store the password. + let entry = Entry::new("dergchat", &username).expect("Failed to create keyring entry."); + entry.set_password(&password).expect("Failed to set password in keyring."); + + // Store the username and default nick in the config. + c.stored_username = username; + c.stored_default_nick = default_nick; + + if let Err(e) = confy::store("dergchat", None, c) { + error!("Failed to store the config file: {}", e); + } + }); +} + +/// Load the configuration from the config file. +/// +/// On failure, log the error and return the default configuration. +pub fn load_config() -> Configuration { + match confy::load("dergchat", None) { + Ok(x) => x, + Err(e) => { + error!("Failed to load config file: {}", e); + Configuration::default() + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 22ffa73..2717174 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,7 @@ mod types; mod widgets; mod xmpp_interface; +mod configuration; use dioxus::prelude::*; use dioxus_desktop::Config; diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index a61edeb..777e1f0 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -18,6 +18,7 @@ use crate::types::LoginCredentials; use crate::widgets::login_screen::LoginAttempt; use crate::widgets::login_screen::{LoginScreen, LoginStatus}; use crate::widgets::room_view::RoomView; +use crate::widgets::no_room_open::NoRoomPlaceholder; use crate::widgets::sidebar::SideBar; use crate::xmpp_interface::NetworkCommand; use dioxus::core::{Element, Scope}; @@ -31,7 +32,7 @@ use std::collections::HashMap; use std::str::FromStr; use std::string::String; use keyring::Entry; -use serde_derive::{Deserialize, Serialize}; +use crate::configuration::load_config; use crate::xmpp_interface; pub mod login_screen; @@ -40,14 +41,10 @@ pub mod room_list; pub mod room_view; pub mod send_message; pub mod sidebar; - -#[derive(Serialize, Deserialize, Default)] -struct Configuration { - stored_username: String, - stored_default_nick: String, -} +mod no_room_open; pub fn App(cx: Scope) -> Element { + let messages = use_ref(cx, || HashMap::new()); let current_room = use_state(cx, || None::); let connection_status = use_state(cx, || LoginStatus::LoggedOut); @@ -56,13 +53,7 @@ pub fn App(cx: Scope) -> Element { xmpp_interface::run_xmpp_toplevel(connection_status.to_owned(), messages.to_owned(), rx) }); - let config = use_ref(cx, || match confy::load("dergchat", None) { - Ok(x) => x, - Err(e) => { - error!("Failed to load config file: {}", e); - Configuration::default() - } - }); + let config = use_ref(cx, load_config); { let config = config.to_owned(); @@ -157,6 +148,9 @@ pub fn App(cx: Scope) -> Element { current_room.set(None); coroutine.send(NetworkCommand::LeaveRoom { room: x }); }, + on_join_room: move |x: BareJid| { + coroutine.send(NetworkCommand::JoinRoom { room: x }); + }, } // The current room. @@ -173,21 +167,11 @@ pub fn App(cx: Scope) -> Element { } } else { // No room selected - NoRoomPlaceholder {} + rsx! { + NoRoomPlaceholder {} + } } } } } -} - -fn NoRoomPlaceholder(cx: Scope) -> Element { - render! { - div { - padding: "5mm", - flex_grow: 1, - display: "flex", - background_color: "#166322", - "No room selected. Pick one from the list on the left." - } - } -} +} \ No newline at end of file diff --git a/src/widgets/no_room_open.rs b/src/widgets/no_room_open.rs new file mode 100644 index 0000000..7c5ee54 --- /dev/null +++ b/src/widgets/no_room_open.rs @@ -0,0 +1,31 @@ +// Dergchat, a free XMPP client. +// Copyright (C) 2023 Werner Kroneman +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use dioxus::core::{Element, Scope}; +use dioxus::prelude::*; + +/// A placeholder widget for when no room is open. +pub fn NoRoomPlaceholder(cx: Scope) -> Element { + render! { + div { + padding: "5mm", + flex_grow: 1, + display: "flex", + background_color: "#166322", + "No room selected. Pick one from the list on the left." + } + } +} diff --git a/src/widgets/room_join_widget.rs b/src/widgets/room_join_widget.rs index 230d072..b860eac 100644 --- a/src/widgets/room_join_widget.rs +++ b/src/widgets/room_join_widget.rs @@ -18,18 +18,25 @@ use dioxus::core::{Element, Scope}; use dioxus::core_macro::Props; use dioxus::hooks::use_state; use dioxus::prelude::*; +use jid::BareJid; +use std::str::FromStr; /// The props for the room join widget, including: /// * The event handler for when the user clicks the join button, passing in a room name. This name is not validated in any way. #[derive(Props)] pub struct RoomJoinProps<'a> { - on_join_room: EventHandler<'a, String>, + on_join_room: EventHandler<'a, BareJid>, } /// A simple widget that allows the user to join a room by typing /// its name into a text field and clicking a button. +/// +/// Also validates the room name, and displays an error message if the room name is invalid. pub fn RoomJoinWidget<'a>(cx: Scope<'a, RoomJoinProps>) -> Element<'a> { + + // Store the current room name and error message in state. let room = use_state(cx, || "".to_owned()); + let error = use_state(cx, || "".to_owned()); render! { div { @@ -39,11 +46,19 @@ pub fn RoomJoinWidget<'a>(cx: Scope<'a, RoomJoinProps>) -> Element<'a> { flex_grow: 1, display: "block", oninput: |evt| { - room.set(evt.value.clone()); + room.set(evt.value.to_string()); + error.set("".to_owned()); } } button { - onclick: move |_| cx.props.on_join_room.call(room.current().to_string()), + onclick: move |_| { + // Validate the room name. If it's a valid Jid, try to join it. + // Otherwise, display an error message. + match BareJid::from_str(room.current().as_str()) { + Ok(jid) => {error.set("".to_string()); cx.props.on_join_room.call(jid);}, + Err(e) => {error.set(e.to_string());}, + } + }, "Join" } } diff --git a/src/widgets/sidebar.rs b/src/widgets/sidebar.rs index d418122..8f4e397 100644 --- a/src/widgets/sidebar.rs +++ b/src/widgets/sidebar.rs @@ -22,14 +22,15 @@ use crate::widgets::room_list::RoomList; use crate::widgets::room_join_widget::RoomJoinWidget; #[derive(Props)] -pub struct SideBarProps { +pub struct SideBarProps<'a> { pub rooms: Vec, - pub on_room_picked: EventHandler<'static, BareJid>, - pub on_room_left: EventHandler<'static, BareJid>, + pub on_room_picked: EventHandler<'a, BareJid>, + pub on_room_left: EventHandler<'a, BareJid>, + pub on_join_room: EventHandler<'a, BareJid>, } /// A widget that combines the RoomList, RoomJoinWidget, and current user info into a sidebar. -pub fn SideBar(cx: Scope) -> Element { +pub fn SideBar<'a>(cx: Scope<'a, SideBarProps>) -> Element<'a> { render! { div { padding: "5mm", @@ -46,15 +47,13 @@ pub fn SideBar(cx: Scope) -> Element { // The list of rooms. RoomList { rooms: cx.props.rooms.clone(), - on_room_picked: cx.props.on_room_picked.clone(), - on_room_left: cx.props.on_room_left.clone(), + on_room_picked: |e| cx.props.on_room_picked.call(e), + on_room_left: |e| cx.props.on_room_left.call(e), } // The widget to join a room. RoomJoinWidget { - on_join_room: |x: String| { - println!("Joining room: {:?}", x); - }, + on_join_room: |x| cx.props.on_join_room.call(x), } } }