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