Refactored the mod.rs, specifically focusing on getting the Sidebar out and some xmpp bits.

This commit is contained in:
Werner Kroneman 2023-12-10 15:54:31 +01:00
parent 1ddf72d8a4
commit 4c857f2fba
3 changed files with 193 additions and 125 deletions

View File

@ -14,13 +14,11 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::types::{LoginCredentials, Message}; use crate::types::LoginCredentials;
use crate::widgets::login_screen::LoginAttempt; use crate::widgets::login_screen::LoginAttempt;
use crate::widgets::login_screen::{LoginScreen, LoginStatus}; 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::widgets::room_view::RoomView;
use crate::xmpp_interface::xmpp_mainloop; use crate::widgets::sidebar::SideBar;
use crate::xmpp_interface::NetworkCommand; use crate::xmpp_interface::NetworkCommand;
use dioxus::core::{Element, Scope}; use dioxus::core::{Element, Scope};
@ -32,71 +30,16 @@ use std::collections::HashMap;
use std::str::FromStr; use std::str::FromStr;
use std::string::String; use std::string::String;
use xmpp::{Agent, ClientBuilder, ClientType};
use keyring::Entry; use keyring::Entry;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use crate::xmpp_interface;
mod login_screen; pub mod login_screen;
mod room_join_widget; pub mod room_join_widget;
mod room_list; pub mod room_list;
mod room_view; pub mod room_view;
mod send_message; pub mod send_message;
pub mod sidebar;
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");
}
}
#[derive(Serialize, Deserialize, Default)] #[derive(Serialize, Deserialize, Default)]
struct Configuration { struct Configuration {
@ -110,7 +53,7 @@ pub fn App(cx: Scope) -> Element {
let connection_status = use_state(cx, || LoginStatus::LoggedOut); let connection_status = use_state(cx, || LoginStatus::LoggedOut);
let coroutine = use_coroutine(cx, |rx: UnboundedReceiver<NetworkCommand>| { let coroutine = use_coroutine(cx, |rx: UnboundedReceiver<NetworkCommand>| {
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) { let config = use_ref(cx, || match confy::load("dergchat", None) {
@ -204,45 +147,24 @@ pub fn App(cx: Scope) -> Element {
rsx!{ rsx!{
// Sidebar. // Sidebar.
div { SideBar {
padding: "5mm",
background_color: "#1d852d",
display: "flex",
flex_direction: "column",
div {
border_bottom: "1px solid lightgray",
"Mizah"
}
RoomList {
rooms: messages.read().keys().cloned().collect(), rooms: messages.read().keys().cloned().collect(),
on_room_picked: move |x: BareJid| { on_room_picked: move |x: BareJid| {
current_room.set(Some(x)); current_room.set(Some(x.clone()));
coroutine.send(NetworkCommand::JoinRoom { room: x });
}, },
on_room_left: move |x: BareJid| { on_room_left: move |x: BareJid| {
current_room.set(None);
coroutine.send(NetworkCommand::LeaveRoom { room: x }); coroutine.send(NetworkCommand::LeaveRoom { room: x });
}, },
} }
RoomJoinWidget { // The current room.
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() { if let Some(room) = current_room.get() {
let messages = messages.read().get(&room).expect("Selected non-existant room").to_vec();
rsx! { rsx! {
RoomView { RoomView {
room: room.clone(), room: room.clone(),
messages: messages, messages: messages.read().get(&room).expect("Selected non-existant room").to_vec(),
on_message_sent: move |x:String| { on_message_sent: move |x:String| {
println!("Message sent: {:?}", x); println!("Message sent: {:?}", x);
coroutine.send(NetworkCommand::SendMessage { recipient: room.clone(), message: x }); coroutine.send(NetworkCommand::SendMessage { recipient: room.clone(), message: x });
@ -251,8 +173,15 @@ pub fn App(cx: Scope) -> Element {
} }
} else { } else {
// No room selected // No room selected
NoRoomPlaceholder {}
}
}
}
}
}
rsx! { fn NoRoomPlaceholder(cx: Scope) -> Element {
render! {
div { div {
padding: "5mm", padding: "5mm",
flex_grow: 1, flex_grow: 1,
@ -261,13 +190,4 @@ pub fn App(cx: Scope) -> Element {
"No room selected. Pick one from the list on the left." "No room selected. Pick one from the list on the left."
} }
} }
}
}
}
}
} }

61
src/widgets/sidebar.rs Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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<BareJid>,
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<SideBarProps>) -> 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);
},
}
}
}
}

View File

@ -15,16 +15,37 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::types::{LoginCredentials, Message}; use crate::types::{LoginCredentials, Message};
use dioxus::hooks::{UnboundedReceiver, UseRef}; use dioxus::hooks::{UnboundedReceiver, UseRef, UseState};
use futures_util::stream::StreamExt; use futures_util::stream::StreamExt;
use jid::BareJid; use jid::BareJid;
use log::info; use log::{error, info};
use std::collections::HashMap; use std::collections::HashMap;
use std::future::Future; use std::future::Future;
use tokio::select; use tokio::select;
use xmpp::parsers::message::MessageType; 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( async fn handle_event(
event: xmpp::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>( pub fn xmpp_mainloop<'a>(
agent: &'a mut Agent, 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<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");
}
}