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),
}
}
}