diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs
index def9e7e..a61edeb 100644
--- a/src/widgets/mod.rs
+++ b/src/widgets/mod.rs
@@ -14,13 +14,11 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-use crate::types::{LoginCredentials, Message};
+use crate::types::LoginCredentials;
use crate::widgets::login_screen::LoginAttempt;
use crate::widgets::login_screen::{LoginScreen, LoginStatus};
-use crate::widgets::room_join_widget::RoomJoinWidget;
-use crate::widgets::room_list::RoomList;
use crate::widgets::room_view::RoomView;
-use crate::xmpp_interface::xmpp_mainloop;
+use crate::widgets::sidebar::SideBar;
use crate::xmpp_interface::NetworkCommand;
use dioxus::core::{Element, Scope};
@@ -32,71 +30,16 @@ use std::collections::HashMap;
use std::str::FromStr;
use std::string::String;
-use xmpp::{Agent, ClientBuilder, ClientType};
-
use keyring::Entry;
use serde_derive::{Deserialize, Serialize};
+use crate::xmpp_interface;
-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,
- mut messages: UseRef>>,
- mut commands: UnboundedReceiver,
-) {
- // 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 mod login_screen;
+pub mod room_join_widget;
+pub mod room_list;
+pub mod room_view;
+pub mod send_message;
+pub mod sidebar;
#[derive(Serialize, Deserialize, Default)]
struct Configuration {
@@ -110,7 +53,7 @@ pub fn App(cx: Scope) -> Element {
let connection_status = use_state(cx, || LoginStatus::LoggedOut);
let coroutine = use_coroutine(cx, |rx: UnboundedReceiver| {
- run_xmpp_toplevel(connection_status.to_owned(), messages.to_owned(), rx)
+ xmpp_interface::run_xmpp_toplevel(connection_status.to_owned(), messages.to_owned(), rx)
});
let config = use_ref(cx, || match confy::load("dergchat", None) {
@@ -204,45 +147,24 @@ pub fn App(cx: Scope) -> Element {
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"),
- });
- },
- }
+ SideBar {
+ rooms: messages.read().keys().cloned().collect(),
+ on_room_picked: move |x: BareJid| {
+ current_room.set(Some(x.clone()));
+ coroutine.send(NetworkCommand::JoinRoom { room: x });
+ },
+ on_room_left: move |x: BareJid| {
+ current_room.set(None);
+ coroutine.send(NetworkCommand::LeaveRoom { room: x });
+ },
}
- // Room view.
-
+ // The current room.
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,
+ messages: messages.read().get(&room).expect("Selected non-existant room").to_vec(),
on_message_sent: move |x:String| {
println!("Message sent: {:?}", x);
coroutine.send(NetworkCommand::SendMessage { recipient: room.clone(), message: x });
@@ -251,23 +173,21 @@ pub fn App(cx: Scope) -> Element {
}
} 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."
- }
- }
-
+ 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."
+ }
}
}
diff --git a/src/widgets/sidebar.rs b/src/widgets/sidebar.rs
new file mode 100644
index 0000000..d418122
--- /dev/null
+++ b/src/widgets/sidebar.rs
@@ -0,0 +1,61 @@
+// 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::core_macro::Props;
+use jid::BareJid;
+use dioxus::prelude::*;
+use crate::widgets::room_list::RoomList;
+use crate::widgets::room_join_widget::RoomJoinWidget;
+
+#[derive(Props)]
+pub struct SideBarProps {
+ pub rooms: Vec,
+ pub on_room_picked: EventHandler<'static, BareJid>,
+ pub on_room_left: EventHandler<'static, BareJid>,
+}
+
+/// A widget that combines the RoomList, RoomJoinWidget, and current user info into a sidebar.
+pub fn SideBar(cx: Scope) -> Element {
+ render! {
+ div {
+ padding: "5mm",
+ background_color: "#1d852d",
+ display: "flex",
+ flex_direction: "column",
+
+ // The name of the current user (TODO: make this actually reflect the current user).
+ div {
+ border_bottom: "1px solid lightgray",
+ "Mizah"
+ }
+
+ // 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(),
+ }
+
+ // The widget to join a room.
+ RoomJoinWidget {
+ on_join_room: |x: String| {
+ println!("Joining room: {:?}", x);
+ },
+ }
+ }
+ }
+}
diff --git a/src/xmpp_interface.rs b/src/xmpp_interface.rs
index a6a44b3..e81c7c9 100644
--- a/src/xmpp_interface.rs
+++ b/src/xmpp_interface.rs
@@ -15,16 +15,37 @@
// along with this program. If not, see .
use crate::types::{LoginCredentials, Message};
-use dioxus::hooks::{UnboundedReceiver, UseRef};
+use dioxus::hooks::{UnboundedReceiver, UseRef, UseState};
use futures_util::stream::StreamExt;
use jid::BareJid;
-use log::info;
+use log::{error, info};
use std::collections::HashMap;
use std::future::Future;
use tokio::select;
use xmpp::parsers::message::MessageType;
-use xmpp::Agent;
+use xmpp::{Agent, ClientBuilder, ClientType};
+use crate::widgets::login_screen::LoginStatus;
+
+/// An enum of commands that can be sent to the XMPP interface.
+///
+/// These very loosely correspond to XMPP stanzas, but are more high level.
+#[derive(Debug)]
+pub enum NetworkCommand {
+
+ /// Start a new login attempt, resulting in either a successful login or an error.
+ TryLogin { credentials: LoginCredentials },
+
+ /// Join a room.
+ JoinRoom { room: BareJid },
+
+ /// Leave a room.
+ LeaveRoom { room: BareJid },
+
+ /// Send a message to a recipient.
+ SendMessage { recipient: BareJid, message: String },
+
+}
async fn handle_event(
event: xmpp::Event,
@@ -87,13 +108,7 @@ async fn handle_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,
@@ -141,3 +156,75 @@ pub fn xmpp_mainloop<'a>(
}
}
}
+
+/// Wait for the agent to go online.
+///
+/// This function mutably borrows an Agent and waits for either an Online or Disconnected event.
+///
+/// The future resolves to Ok(()) if the agent went online, or Err(String) if the agent disconnected.
+async fn await_online(agent: &mut Agent) -> Result<(), String> {
+
+ // Wait for the next batch of events.
+ let events = agent
+ .wait_for_events()
+ .await
+ .ok_or("Stream closed unexpectedly".to_string())?;
+
+ // We expect exactly one event; we operate under the assumption that xmpp-rs emits
+ // exactly one events when it receives a connect/disconnect stanza from the server,
+ // and that that is the first stanza it receives.
+ // TODO: This is brittle; need to talk to the xmpp-rs to get stronger guarantees.
+ assert_eq!(events.len(), 1);
+
+ // We expect the first event to be either Online or Disconnected.
+ match events.into_iter().next().expect("No events") {
+ xmpp::Event::Online => {
+ info!("Login attempt successful; connected.");
+ Ok(())
+ }
+ xmpp::Event::Disconnected => {
+ error!("Login attempt resulted in disconnect.");
+ Err("Disconnected".to_string())
+ }
+ // We don't expect any other events; if we get one, we panic. (The assumption we made must be wrong.)
+ // TODO: This is ugly.
+ e => {
+ panic!("Unexpected event: {:?}", e);
+ }
+ }
+}
+
+pub async fn run_xmpp_toplevel(
+ connection_status: UseState,
+ mut messages: UseRef>>,
+ mut commands: UnboundedReceiver,
+) {
+ // 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");
+ }
+}