diff --git a/src/types.rs b/src/types.rs deleted file mode 100644 index 7d74505..0000000 --- a/src/types.rs +++ /dev/null @@ -1,39 +0,0 @@ -// 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 crate::passwords::Password; -use jid::BareJid; -use std::fmt::Debug; -use chrono::{DateTime, FixedOffset}; - -#[derive(Debug, Clone, PartialEq)] -pub struct Room { - pub messages: Vec, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Message { - pub sender: String, - pub body: String, - pub timestamp: DateTime -} - -#[derive(Debug)] -pub struct LoginCredentials { - pub username: BareJid, - pub default_nick: String, - pub password: Password, -} diff --git a/src/types/message.rs b/src/types/message.rs new file mode 100644 index 0000000..b809d25 --- /dev/null +++ b/src/types/message.rs @@ -0,0 +1,93 @@ +// 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 chrono::{DateTime, FixedOffset}; +use crate::types::{DirectMessage, MessageDeliveryError, MucMessage, Nickname}; + +/// An outgoing status for a message. +/// +/// See: https://docs.modernxmpp.org/client/design/#conversation-view +#[derive(Debug, Clone, PartialEq)] +pub enum OutgoingMessageStatus { + PendingDelivery, + DeliveredToServer, + DeliveredToContact, + ReadByContact, + DeliveryError(MessageDeliveryError), +} + +/// An enum type signaling whether a message is incoming or outgoing. +#[derive(Debug, Clone, PartialEq)] +pub enum MessageInOut { + /// An incoming message. + Incoming, + /// An outgoing message, with a status. + Outgoing(OutgoingMessageStatus), +} + +/// A message, either incoming or outgoing. +/// +/// This is a trait, because MUC messages and direct messages are subtly different. +pub trait Message { + + /// Whether the message is incoming or outgoing. + /// If outgoing, also contains the status of the message. + fn in_out(&self) -> &MessageInOut; + + /// The body of the message, as a string. (TODO: Language?) + fn body(&self) -> &String; + + /// The timestamp of the message; when it was sent originally. + fn timestamp(&self) -> &DateTime; + + fn sender_nick(&self) -> Nickname; +} + +impl Message for MucMessage { + fn in_out(&self) -> &MessageInOut { + &self.in_out + } + + fn body(&self) -> &String { + &self.body + } + + fn timestamp(&self) -> &DateTime { + &self.timestamp + } + + fn sender_nick(&self) -> Nickname { + self.sender_nick.clone() + } +} + +impl Message for DirectMessage { + fn in_out(&self) -> &MessageInOut { + &self.in_out + } + + fn body(&self) -> &String { + &self.body + } + + fn timestamp(&self) -> &DateTime { + &self.timestamp + } + + fn sender_nick(&self) -> Nickname { + Nickname("Not implemented.".to_string()) + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..a571671 --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,89 @@ +// 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 crate::passwords::Password; +use jid::BareJid; +use std::fmt::Debug; +use chrono::{DateTime, FixedOffset}; +use message::{Message, MessageInOut}; + +pub mod message; +pub mod room; + +#[derive(Debug, Clone, PartialEq)] +pub struct DirectMessageRoom { + pub messages: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct MucRoom { + pub messages: Vec, +} + +pub trait Room { + type Message: Message; + + fn messages(&self) -> &Vec; +} + +impl Room for DirectMessageRoom { + type Message = DirectMessage; + + fn messages(&self) -> &Vec { + &self.messages + } +} + +impl Room for MucRoom { + type Message = MucMessage; + + fn messages(&self) -> &Vec { + &self.messages + } +} + +/// A nickname for a user. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Nickname(pub String); + +#[derive(Debug, Clone, PartialEq)] +pub struct MucMessage { + pub in_out: MessageInOut, + pub sender_nick: Nickname, + pub body: String, + pub timestamp: DateTime, + pub is_private: bool, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct DirectMessage { + pub in_out: MessageInOut, + pub body: String, + pub timestamp: DateTime, +} + +#[derive(Debug)] +pub struct LoginCredentials { + pub username: BareJid, + pub default_nick: String, + pub password: Password, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct MessageDeliveryError { + pub error: String, +} + diff --git a/src/types/room.rs b/src/types/room.rs new file mode 100644 index 0000000..5bca4b3 --- /dev/null +++ b/src/types/room.rs @@ -0,0 +1,66 @@ +// 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 . + +// 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 crate::types::{DirectMessageRoom, MucRoom}; + +#[derive(Debug, Clone, PartialEq)] +pub enum Room { + Direct(DirectMessageRoom), + Muc(MucRoom), +} + +#[derive(Debug, Clone, PartialEq)] +pub struct WrongRoomType; + +impl<'a> TryInto<&'a mut DirectMessageRoom> for &'a mut Room { + type Error = WrongRoomType; + + fn try_into(self) -> Result<&'a mut DirectMessageRoom, Self::Error> { + match self { + Room::Direct(room) => Ok(room), + _ => Err(WrongRoomType), + } + } + +} + +impl<'a> TryInto<&'a mut MucRoom> for &'a mut Room { + type Error = WrongRoomType; + + fn try_into(self) -> Result<&'a mut MucRoom, Self::Error> { + match self { + Room::Muc(room) => Ok(room), + _ => Err(WrongRoomType), + } + } + +} diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index ad938ee..8e00a85 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -34,8 +34,9 @@ use dioxus::prelude::*; use futures_util::StreamExt; use jid::BareJid; use log::{error, info}; -use std::collections::HashMap; use std::string::String; +use crate::xmpp_interface::Messages; +use crate::types::room::Room; pub mod login_screen; pub mod no_room_open; @@ -46,7 +47,7 @@ pub mod send_message; pub mod sidebar; pub fn App(cx: Scope) -> Element { - let messages = use_ref(cx, || HashMap::new()); + let messages = use_ref(cx, || Messages::new()); let current_room = use_state(cx, || None::); let connection_status = use_state(cx, || LoginStatus::LoggedOut); @@ -73,23 +74,35 @@ pub fn App(cx: Scope) -> Element { } }); + // Can I not clone this? Am I reconstructing the entire VDOM every time? What's happening? + // TODO: The borrowing situation here is kinda fucked; how do I fix this? + let current_room_messages = current_room.get().clone().and_then(|x| messages.read().messages.get(&x).map(|x| x.clone())); + render! { // If not logged in, show a login screen. if let LoginStatus::LoggedIn(user) = connection_status.get() { + let room_list = messages.read().messages.iter().map(|(k, v)| { + match v { + Room::Direct(d) => RoomMeta { + room: k.clone(), + last_update_time: d.messages.last().map(|x| x.timestamp).map(|x| x.clone()), + }, + Room::Muc(m) => RoomMeta { + room: k.clone(), + last_update_time: m.messages.last().map(|x| x.timestamp).map(|x| x.clone()), + }, + } + }).collect(); + // We're logged in; show a sidebar and a room view. rsx!{ // Sidebar. SideBar { current_user: user.clone(), - rooms: messages.read().iter().map(|(k, v)| { - RoomMeta { - room: k.clone(), - last_update_time: v.last().map(|x| x.timestamp), - } - }).collect(), + rooms: room_list, on_room_picked: move |x: BareJid| { current_room.set(Some(x.clone())); coroutine.send(NetworkCommand::JoinRoom { room: x }); @@ -109,16 +122,34 @@ pub fn App(cx: Scope) -> Element { } // The current room. - if let Some(room) = current_room.get() { - rsx! { - RoomView { - room: room.clone(), - 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 }); - }, + if let Some(room) = current_room_messages { + match (room) { + Room::Direct(r) => { + // Direct message room + rsx! { + RoomView { + room: current_room.get().clone().unwrap(), + messages: r, + on_message_sent: move |x:String| { + println!("Message sent: {:?}", x); + coroutine.send(NetworkCommand::SendMessage { recipient: current_room.get().clone().unwrap(), message: x }); + }, + } } + }, + Room::Muc(r) => { + // MUC room + rsx! { + RoomView { + room: current_room.get().clone().unwrap(), + messages: r, + on_message_sent: move |x:String| { + println!("Message sent: {:?}", x); + coroutine.send(NetworkCommand::SendMessage { recipient: current_room.get().clone().unwrap(), message: x }); + }, + } + } + }, } } else { // No room selected diff --git a/src/widgets/room_view.rs b/src/widgets/room_view.rs index cdfc577..db8a8f8 100644 --- a/src/widgets/room_view.rs +++ b/src/widgets/room_view.rs @@ -14,19 +14,20 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use crate::types::Message; +use crate::types::message::Message; use crate::widgets::send_message::SendMessage; use dioxus::core::{Element, Scope}; use dioxus::core_macro::Props; use dioxus::hooks::use_state; use dioxus::prelude::*; use jid::BareJid; +use crate::types::Room; /// A widget that shows a room, including a list of messages and a widget to send new messages. #[component] -pub fn RoomView<'a>(cx: Scope<'a>, +pub fn RoomView<'a, R:Room>(cx: Scope<'a>, room: BareJid, - messages: Vec, + messages: R, on_message_sent: EventHandler<'a, String>) -> Element<'a> { render! { @@ -49,9 +50,9 @@ pub fn RoomView<'a>(cx: Scope<'a>, ul { flex_grow: 1, overflow_y: "scroll", - for message in cx.props.messages.iter() { + for message in cx.props.messages.messages().iter() { rsx! { li { - "{message.sender}: {message.body}" + "{message.sender_nick().0}: {message.body()}" } } } } diff --git a/src/xmpp_interface.rs b/src/xmpp_interface.rs index 90ffade..cdae3b6 100644 --- a/src/xmpp_interface.rs +++ b/src/xmpp_interface.rs @@ -14,19 +14,19 @@ // 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::{DirectMessage, DirectMessageRoom, LoginCredentials, MucMessage, MucRoom, Nickname}; use dioxus::hooks::{UnboundedReceiver, UseRef, UseState}; use futures_util::stream::StreamExt; use jid::BareJid; -use log::{error, info}; +use log::{debug, error, info, warn}; use std::collections::HashMap; use std::future::Future; -use std::time::Instant; - use crate::widgets::login_screen::LoginStatus; use tokio::select; use xmpp::parsers::message::MessageType; use xmpp::{Agent, ClientBuilder, ClientType}; +use crate::types::room::{Room, WrongRoomType}; +use crate::types::message::MessageInOut; /// An enum of commands that can be sent to the XMPP interface. /// @@ -48,73 +48,124 @@ pub enum NetworkCommand { Logout, } +#[derive(Debug, Clone, PartialEq)] +pub struct Messages { + pub messages: HashMap, +} + +impl Messages { + + pub fn new() -> Self { + Self { + messages: HashMap::new(), + } + } + + pub fn get(&self, room: &BareJid) -> Option<&Room> { + self.messages.get(room) + } + + pub fn get_or_create_mut_direct(&mut self, room: &BareJid) -> Result<&mut DirectMessageRoom, WrongRoomType> { + self.messages.entry(room.clone()).or_insert(Room::Direct(DirectMessageRoom { + messages: vec![], + })).try_into() + } + + pub fn get_or_create_mut_muc(&mut self, room: &BareJid) -> Result<&mut MucRoom, WrongRoomType> { + self.messages.entry(room.clone()).or_insert(Room::Muc(MucRoom { + messages: vec![], + })).try_into() + } + +} + async fn handle_event( event: xmpp::Event, agent: &mut xmpp::Agent, - messages: &mut UseRef>>, + messages: &mut UseRef, ) { match event { xmpp::Event::JoinRoom(jid, _conference) => { - println!("Will auto-join room: {}", &jid); + debug!("Will auto-join room: {}", &jid); agent.join_room(jid, None, None, "en/us", "online").await; } xmpp::Event::RoomJoined(room_jid) => { - println!("Joined room: {}", &room_jid); + debug!("Joined room: {}", &room_jid); messages.with_mut(move |m| { - m.entry(room_jid.clone()).or_insert(vec![]); + m.messages.entry(room_jid.clone()).or_insert(Room::Muc(MucRoom { + messages: vec![], + })); }); } xmpp::Event::RoomLeft(room_jid) => { - println!("Left room: {}", &room_jid); + debug!("Left room: {}", &room_jid); messages.with_mut(move |m| { - m.remove(&room_jid); + m.messages.remove(&room_jid); }); } xmpp::Event::ChatMessage(_id, sender, body, timestamp) => { - println!("Message from {}: {}", &sender, &body.0); + debug!("Message from {}: {}", &sender, &body.0); messages.with_mut(move |m| { - m.entry(sender.clone()).or_insert(vec![]).push(Message { - sender: sender.to_string(), + let dms : &mut DirectMessageRoom = m.messages.entry(sender.clone()).or_insert(Room::Direct(DirectMessageRoom { + messages: vec![], + })).try_into().expect("Received direct message with a JID from a MUC"); + + dms.messages.push(DirectMessage { + in_out: MessageInOut::Incoming, body: body.0, timestamp, }); }); } xmpp::Event::RoomMessage(_id, room_jid, sender_nick, body, timestamp) => { - println!( + debug!( "Message in {} from {}: {}", &room_jid, &sender_nick, &body.0 ); messages.with_mut(move |m| { - m.entry(room_jid.clone()).or_insert(vec![]).push(Message { - sender: sender_nick, + + let muc : &mut MucRoom = m.get_or_create_mut_muc(&room_jid) + .expect("Received direct message with a JID from a MUC"); + + muc.messages.push(MucMessage { + in_out: MessageInOut::Incoming, + sender_nick: Nickname(sender_nick.to_string()), body: body.0, timestamp, - }) + is_private: false, + }); + }); } xmpp::Event::RoomPrivateMessage(_id, room_jid, sender_nick, body, timestamp) => { - println!( + debug!( "Private message in {} from {}: {}", &room_jid, &sender_nick, &body.0 ); messages.with_mut(move |m| { - m.entry(room_jid.clone()).or_insert(vec![]).push(Message { - sender: sender_nick, + + let muc : &mut MucRoom = m.get_or_create_mut_muc(&room_jid) + .expect("Received direct message with a JID from a MUC"); + + muc.messages.push(MucMessage { + in_out: MessageInOut::Incoming, + sender_nick: Nickname(sender_nick.to_string()), body: body.0, timestamp, - }) + is_private: true, + }); + }); } _ => { - log::debug!("Received unsupported event {:?}", event); + warn!("Received unsupported event {:?}", event); } } } pub fn xmpp_mainloop<'a>( agent: &'a mut Agent, - mut room_data: &'a mut UseRef>>, + mut room_data: &'a mut UseRef, commands: &'a mut UnboundedReceiver, ) -> impl Future + Sized + 'a { async move { @@ -203,7 +254,7 @@ async fn await_online(agent: &mut Agent) -> Result<(), String> { pub async fn run_xmpp_toplevel( connection_status: UseState, - mut messages: UseRef>>, + mut messages: UseRef, mut commands: UnboundedReceiver, ) { // Await a login attempt: