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
// 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::{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<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");
}
}
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<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) {
@ -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 {
SideBar {
rooms: messages.read().keys().cloned().collect(),
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| {
current_room.set(None);
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"),
});
},
}
}
// 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,8 +173,15 @@ pub fn App(cx: Scope) -> Element {
}
} else {
// No room selected
NoRoomPlaceholder {}
}
}
}
}
}
rsx! {
fn NoRoomPlaceholder(cx: Scope) -> Element {
render! {
div {
padding: "5mm",
flex_grow: 1,
@ -261,13 +190,4 @@ pub fn App(cx: Scope) -> Element {
"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/>.
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<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");
}
}