Added dedicated types to Room/MucRoom/Direct messages, etc...

It's still a bit of a mess though.
This commit is contained in:
Werner Kroneman 2023-12-11 16:51:45 +01:00
parent 53ad01ed41
commit 837dca3e15
7 changed files with 377 additions and 85 deletions

View File

@ -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 <https://www.gnu.org/licenses/>.
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<Message>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Message {
pub sender: String,
pub body: String,
pub timestamp: DateTime<FixedOffset>
}
#[derive(Debug)]
pub struct LoginCredentials {
pub username: BareJid,
pub default_nick: String,
pub password: Password,
}

93
src/types/message.rs Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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<FixedOffset>;
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<FixedOffset> {
&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<FixedOffset> {
&self.timestamp
}
fn sender_nick(&self) -> Nickname {
Nickname("Not implemented.".to_string())
}
}

89
src/types/mod.rs Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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<DirectMessage>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MucRoom {
pub messages: Vec<MucMessage>,
}
pub trait Room {
type Message: Message;
fn messages(&self) -> &Vec<Self::Message>;
}
impl Room for DirectMessageRoom {
type Message = DirectMessage;
fn messages(&self) -> &Vec<Self::Message> {
&self.messages
}
}
impl Room for MucRoom {
type Message = MucMessage;
fn messages(&self) -> &Vec<Self::Message> {
&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<FixedOffset>,
pub is_private: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DirectMessage {
pub in_out: MessageInOut,
pub body: String,
pub timestamp: DateTime<FixedOffset>,
}
#[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,
}

66
src/types/room.rs Normal file
View File

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

View File

@ -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::<BareJid>);
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

View File

@ -14,19 +14,20 @@
// 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::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<Message>,
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()}"
} }
}
}

View File

@ -14,19 +14,19 @@
// 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::{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<BareJid, Room>,
}
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<HashMap<BareJid, Vec<Message>>>,
messages: &mut UseRef<Messages>,
) {
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<HashMap<BareJid, Vec<Message>>>,
mut room_data: &'a mut UseRef<Messages>,
commands: &'a mut UnboundedReceiver<NetworkCommand>,
) -> impl Future<Output = ()> + 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<LoginStatus>,
mut messages: UseRef<HashMap<BareJid, Vec<Message>>>,
mut messages: UseRef<Messages>,
mut commands: UnboundedReceiver<NetworkCommand>,
) {
// Await a login attempt: