From 4c857f2fba7ebb628af71ccc1853f026078b7435 Mon Sep 17 00:00:00 2001 From: Werner Kroneman Date: Sun, 10 Dec 2023 15:54:31 +0100 Subject: [PATCH] Refactored the mod.rs, specifically focusing on getting the Sidebar out and some xmpp bits. --- src/widgets/mod.rs | 150 ++++++++++------------------------------- src/widgets/sidebar.rs | 61 +++++++++++++++++ src/xmpp_interface.rs | 107 ++++++++++++++++++++++++++--- 3 files changed, 193 insertions(+), 125 deletions(-) create mode 100644 src/widgets/sidebar.rs 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"); + } +}