[fractal/fractal-next] content: Send room messages
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal/fractal-next] content: Send room messages
- Date: Wed, 5 May 2021 07:43:37 +0000 (UTC)
commit d529e0d57675ccad1a29cfe5144ba95814460e4c
Author: Julian Sparber <julian sparber net>
Date: Mon May 3 18:09:05 2021 +0200
content: Send room messages
data/resources/style.css | 12 +++++
data/resources/ui/content.ui | 22 ++++++++-
src/session/content/content.rs | 47 ++++++++++++++++++-
src/session/room/event.rs | 47 +++++++++++++------
src/session/room/item.rs | 6 +--
src/session/room/room.rs | 101 ++++++++++++++++++++++++++++++++++++++++-
src/session/room/timeline.rs | 61 +++++++++++++++++++++----
7 files changed, 265 insertions(+), 31 deletions(-)
---
diff --git a/data/resources/style.css b/data/resources/style.css
index 650883e3..a3218d67 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -59,3 +59,15 @@
background-color: @text_view_bg;
color: @theme_text_color;
}
+
+.message-entry > .view {
+ background-color: @theme_base_color;
+ border-radius: 5px;
+ border: 1px solid @borders;
+ padding: 6px;
+}
+
+.message-entry > .view:focus {
+ border: 2px solid @theme_selected_bg_color;
+ padding: 5px;
+}
diff --git a/data/resources/ui/content.ui b/data/resources/ui/content.ui
index 298c1fd1..f7eda40f 100644
--- a/data/resources/ui/content.ui
+++ b/data/resources/ui/content.ui
@@ -8,7 +8,7 @@
<property name="orientation">vertical</property>
<child>
<object class="AdwHeaderBar" id="headerbar">
- <property name="show-start-title-buttons" bind-source="Content" bind-property="compact"
bind-flags="sync-create" />
+ <property name="show-start-title-buttons" bind-source="Content" bind-property="compact"
bind-flags="sync-create"/>
<child type="end">
<object class="GtkMenuButton" id="room_menu">
<property name="icon-name">view-more-symbolic</property>
@@ -78,6 +78,7 @@
<child>
<object class="GtkButton">
<property name="icon-name">mail-attachment-symbolic</property>
+ <property name="action-name">content.select-file</property>
</object>
</child>
<child>
@@ -86,13 +87,30 @@
</object>
</child>
<child>
- <object class="GtkEntry" id="message_entry">
+ <object class="GtkScrolledWindow" id="scrolled_window">
+ <property name="vexpand">True</property>
<property name="hexpand">True</property>
+ <property name="vscrollbar-policy">external</property>
+ <property name="max-content-height">200</property>
+ <property name="propagate-natural-height">True</property>
+ <property name="child">
+ <object class="GtkSourceView" id="message_entry">
+ <property name="hexpand">True</property>
+ </object>
+ </property>
+ <style>
+ <class name="message-entry"/>
+ </style>
</object>
</child>
<child>
<object class="GtkButton">
<property name="icon-name">send-symbolic</property>
+ <property name="focus-on-click">False</property>
+ <property name="action-name">content.send-text-message</property>
+ <style>
+ <class name="suggested-action"/>
+ </style>
</object>
</child>
</object>
diff --git a/src/session/content/content.rs b/src/session/content/content.rs
index cc1f6c7e..a361b742 100644
--- a/src/session/content/content.rs
+++ b/src/session/content/content.rs
@@ -1,5 +1,8 @@
use adw::subclass::prelude::*;
-use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
+use gtk::{
+ gdk, glib, glib::clone, glib::signal::Inhibit, prelude::*, subclass::prelude::*,
+ CompositeTemplate,
+};
use crate::session::{content::ItemRow, room::Room};
@@ -13,12 +16,15 @@ mod imp {
pub struct Content {
pub compact: Cell<bool>,
pub room: RefCell<Option<Room>>,
+ pub md_enabled: Cell<bool>,
#[template_child]
pub headerbar: TemplateChild<adw::HeaderBar>,
#[template_child]
pub listview: TemplateChild<gtk::ListView>,
#[template_child]
pub scrolled_window: TemplateChild<gtk::ScrolledWindow>,
+ #[template_child]
+ pub message_entry: TemplateChild<sourceview::View>,
}
#[glib::object_subclass]
@@ -31,6 +37,10 @@ mod imp {
ItemRow::static_type();
Self::bind_template(klass);
klass.set_accessible_role(gtk::AccessibleRole::Group);
+
+ klass.install_action("content.send-text-message", None, move |widget, _, _| {
+ widget.send_text_message();
+ });
}
fn instance_init(obj: &InitializingObject<Self>) {
@@ -105,6 +115,28 @@ mod imp {
}
}));
+ let key_events = gtk::EventControllerKey::new();
+ self.message_entry.add_controller(&key_events);
+
+ key_events
+ .connect_key_pressed(clone!(@weak obj => @default-return Inhibit(false), move |_, key, _,
modifier| {
+ if !modifier.contains(gdk::ModifierType::SHIFT_MASK) && (key == gdk::keys::constants::Return
|| key == gdk::keys::constants::KP_Enter) {
+ obj.activate_action("content.send-text-message", None);
+ Inhibit(true)
+ } else {
+ Inhibit(false)
+ }
+ }));
+ self.message_entry.buffer().connect_property_text_notify(
+ clone!(@weak obj => move |buffer| {
+ let (start_iter, end_iter) = buffer.bounds();
+ obj.action_set_enabled("content.send-text-message", start_iter != end_iter);
+ }),
+ );
+
+ let (start_iter, end_iter) = self.message_entry.buffer().bounds();
+ obj.action_set_enabled("content.send-text-message", start_iter != end_iter);
+
self.parent_constructed(obj);
}
}
@@ -144,4 +176,17 @@ impl Content {
let priv_ = imp::Content::from_instance(self);
priv_.room.borrow().clone()
}
+
+ pub fn send_text_message(&self) {
+ let priv_ = imp::Content::from_instance(self);
+ let buffer = priv_.message_entry.buffer();
+ let (start_iter, end_iter) = buffer.bounds();
+ let body = buffer.text(&start_iter, &end_iter, true);
+
+ if let Some(room) = &*priv_.room.borrow() {
+ room.send_text_message(body.as_str(), priv_.md_enabled.get());
+ }
+
+ buffer.set_text("");
+ }
}
diff --git a/src/session/room/event.rs b/src/session/room/event.rs
index 1da29c8a..534a2b71 100644
--- a/src/session/room/event.rs
+++ b/src/session/room/event.rs
@@ -11,6 +11,7 @@ use matrix_sdk::{
use crate::fn_event;
use crate::session::User;
+use std::cell::RefCell;
#[derive(Clone, Debug, glib::GBoxed)]
#[gboxed(type_name = "BoxedAnyRoomEvent")]
@@ -24,7 +25,7 @@ mod imp {
#[derive(Debug, Default)]
pub struct Event {
- pub event: OnceCell<AnyRoomEvent>,
+ pub event: OnceCell<RefCell<AnyRoomEvent>>,
pub relates_to: RefCell<Vec<super::Event>>,
pub show_header: Cell<bool>,
pub sender: OnceCell<User>,
@@ -53,7 +54,7 @@ mod imp {
"event",
"The matrix event of this Event",
BoxedAnyRoomEvent::static_type(),
- glib::ParamFlags::WRITABLE | glib::ParamFlags::CONSTRUCT_ONLY,
+ glib::ParamFlags::WRITABLE | glib::ParamFlags::CONSTRUCT,
),
glib::ParamSpec::new_boolean(
"show-header",
@@ -92,7 +93,7 @@ mod imp {
match pspec.name() {
"event" => {
let event = value.get::<BoxedAnyRoomEvent>().unwrap();
- self.event.set(event.0).unwrap();
+ obj.set_matrix_event(event.0);
}
"show-header" => {
let show_header = value.get().unwrap();
@@ -139,32 +140,45 @@ impl Event {
priv_.sender.get().unwrap()
}
- pub fn matrix_event(&self) -> &AnyRoomEvent {
+ pub fn matrix_event(&self) -> AnyRoomEvent {
let priv_ = imp::Event::from_instance(&self);
- priv_.event.get().unwrap()
+ priv_.event.get().unwrap().borrow().clone()
}
- pub fn matrix_sender(&self) -> &UserId {
+ pub fn set_matrix_event(&self, event: AnyRoomEvent) {
let priv_ = imp::Event::from_instance(&self);
- let event = priv_.event.get().unwrap();
- fn_event!(event, sender)
+ if let Some(value) = priv_.event.get() {
+ value.replace(event);
+ } else {
+ priv_.event.set(RefCell::new(event)).unwrap();
+ }
+ self.notify("event");
}
- pub fn matrix_event_id(&self) -> &EventId {
+ pub fn matrix_sender(&self) -> UserId {
let priv_ = imp::Event::from_instance(&self);
- let event = priv_.event.get().unwrap();
- fn_event!(event, event_id)
+ let event = &*priv_.event.get().unwrap().borrow();
+ fn_event!(event, sender).clone()
+ }
+
+ pub fn matrix_event_id(&self) -> EventId {
+ let priv_ = imp::Event::from_instance(&self);
+ let event = &*priv_.event.get().unwrap().borrow();
+ fn_event!(event, event_id).clone()
}
pub fn timestamp(&self) -> DateTime<Local> {
let priv_ = imp::Event::from_instance(&self);
- let event = priv_.event.get().unwrap();
+ let event = &*priv_.event.get().unwrap().borrow();
+
fn_event!(event, origin_server_ts).clone().into()
}
/// Find the related event if any
pub fn related_matrix_event(&self) -> Option<EventId> {
- match self.matrix_event() {
+ let priv_ = imp::Event::from_instance(&self);
+
+ match *priv_.event.get().unwrap().borrow() {
AnyRoomEvent::Message(ref message) => match message {
AnyMessageEvent::RoomRedaction(event) => Some(event.redacts.clone()),
_ => match message.content() {
@@ -189,11 +203,13 @@ impl Event {
/// Whether this event is hidden from the user or displayed in the room history.
pub fn is_hidden_event(&self) -> bool {
+ let priv_ = imp::Event::from_instance(&self);
+
if self.related_matrix_event().is_some() {
return true;
}
- match self.matrix_event() {
+ match &*priv_.event.get().unwrap().borrow() {
AnyRoomEvent::Message(message) => match message {
AnyMessageEvent::CallAnswer(_) => true,
AnyMessageEvent::CallInvite(_) => true,
@@ -286,7 +302,8 @@ impl Event {
pub fn can_hide_header(&self) -> bool {
let priv_ = imp::Event::from_instance(&self);
- match priv_.event.get().unwrap() {
+
+ match &*priv_.event.get().unwrap().borrow() {
AnyRoomEvent::Message(ref message) => match message.content() {
AnyMessageEventContent::RoomMessage(message) => match message.msgtype {
MessageType::Audio(_) => true,
diff --git a/src/session/room/item.rs b/src/session/room/item.rs
index 64db4442..abc56765 100644
--- a/src/session/room/item.rs
+++ b/src/session/room/item.rs
@@ -142,7 +142,7 @@ impl Item {
}
}
- pub fn matrix_event(&self) -> Option<&AnyRoomEvent> {
+ pub fn matrix_event(&self) -> Option<AnyRoomEvent> {
let priv_ = imp::Item::from_instance(&self);
if let ItemType::Event(event) = priv_.type_.get().unwrap() {
Some(event.matrix_event())
@@ -163,7 +163,7 @@ impl Item {
pub fn matrix_sender(&self) -> Option<UserId> {
let priv_ = imp::Item::from_instance(&self);
if let ItemType::Event(event) = priv_.type_.get().unwrap() {
- Some(event.matrix_sender().clone())
+ Some(event.matrix_sender())
} else {
None
}
@@ -173,7 +173,7 @@ impl Item {
let priv_ = imp::Item::from_instance(&self);
if let ItemType::Event(event) = priv_.type_.get().unwrap() {
- Some(event.matrix_event_id().clone())
+ Some(event.matrix_event_id())
} else {
None
}
diff --git a/src/session/room/room.rs b/src/session/room/room.rs
index d461ac21..dbe30ebd 100644
--- a/src/session/room/room.rs
+++ b/src/session/room/room.rs
@@ -1,12 +1,24 @@
+use comrak::{markdown_to_html, ComrakOptions};
use gettextrs::gettext;
use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
use log::{error, warn};
use matrix_sdk::{
- events::{room::member::MemberEventContent, AnyRoomEvent, AnyStateEvent, StateEvent},
- identifiers::UserId,
+ events::{
+ room::{
+ member::MemberEventContent,
+ message::{
+ EmoteMessageEventContent, FormattedBody, MessageEventContent, MessageType,
+ TextMessageEventContent,
+ },
+ },
+ AnyMessageEvent, AnyRoomEvent, AnyStateEvent, MessageEvent, StateEvent, Unsigned,
+ },
+ identifiers::{EventId, UserId},
room::Room as MatrixRoom,
+ uuid::Uuid,
RoomMember,
};
+use std::time::SystemTime;
use crate::session::{
categories::CategoryType,
@@ -30,6 +42,8 @@ mod imp {
pub category: Cell<CategoryType>,
pub timeline: OnceCell<Timeline>,
pub room_members: RefCell<HashMap<UserId, User>>,
+ /// The user of this room
+ pub user_id: OnceCell<UserId>,
}
#[glib::object_subclass]
@@ -366,4 +380,87 @@ impl Room {
);
*/
}
+
+ pub fn send_text_message(&self, body: &str, markdown_enabled: bool) {
+ use std::convert::TryFrom;
+ let priv_ = imp::Room::from_instance(self);
+ if let MatrixRoom::Joined(matrix_room) = priv_.matrix_room.get().unwrap().clone() {
+ let is_emote = body.starts_with("/me ");
+
+ // Don't use markdown for emotes
+ let body = if is_emote {
+ body.trim_start_matches("/me ")
+ } else {
+ body
+ };
+
+ let formatted = if markdown_enabled {
+ let mut md_options = ComrakOptions::default();
+ md_options.render.hardbreaks = true;
+ Some(markdown_to_html(&body, &md_options))
+ } else {
+ None
+ };
+
+ let content = if is_emote {
+ let emote = EmoteMessageEventContent {
+ body: body.to_string(),
+ formatted: formatted
+ .filter(|formatted| formatted.as_str() == body)
+ .map(|f| FormattedBody::html(f)),
+ };
+ MessageEventContent::new(MessageType::Emote(emote))
+ } else {
+ let text = if let Some(formatted) =
+ formatted.filter(|formatted| formatted.as_str() == body)
+ {
+ TextMessageEventContent::html(body, formatted)
+ } else {
+ TextMessageEventContent::plain(body)
+ };
+ MessageEventContent::new(MessageType::Text(text))
+ };
+
+ let txn_id = Uuid::new_v4();
+
+ let pending_event = AnyMessageEvent::RoomMessage(MessageEvent {
+ content,
+ event_id: EventId::try_from(format!("${}:fractal.gnome.org", txn_id)).unwrap(),
+ sender: self.user().user_id().clone(),
+ origin_server_ts: SystemTime::now(),
+ room_id: matrix_room.room_id().clone(),
+ unsigned: Unsigned::default(),
+ });
+
+ self.send_message(txn_id, pending_event);
+ }
+ }
+
+ pub fn send_message(&self, txn_id: Uuid, event: AnyMessageEvent) {
+ let priv_ = imp::Room::from_instance(self);
+ let content = event.content();
+
+ if let MatrixRoom::Joined(matrix_room) = priv_.matrix_room.get().unwrap().clone() {
+ let pending_id = event.event_id().clone();
+ priv_
+ .timeline
+ .get()
+ .unwrap()
+ .append_pending(AnyRoomEvent::Message(event));
+
+ do_async(
+ async move { matrix_room.send(content, Some(txn_id)).await },
+ clone!(@weak self as obj => move |result| async move {
+ // FIXME: We should retry the request if it fails
+ match result {
+ Ok(result) => {
+ let priv_ = imp::Room::from_instance(&obj);
+ priv_.timeline.get().unwrap().set_event_id_for_pending(pending_id,
result.event_id)
+ },
+ Err(error) => error!("Couldn't send message: {}", error),
+ };
+ }),
+ );
+ }
+ }
}
diff --git a/src/session/room/timeline.rs b/src/session/room/timeline.rs
index 4efa9680..9f70f6d3 100644
--- a/src/session/room/timeline.rs
+++ b/src/session/room/timeline.rs
@@ -19,6 +19,8 @@ mod imp {
pub list: RefCell<VecDeque<Item>>,
/// A Hashmap linking `EventId` to correspondenting `Event`
pub event_map: RefCell<HashMap<EventId, Event>>,
+ /// Maps the temporary `EventId` of the pending Event to the real `EventId`
+ pub pending_events: RefCell<HashMap<EventId, EventId>>,
}
#[glib::object_subclass]
@@ -198,7 +200,7 @@ impl Timeline {
}
}
- if let Some(relates_to) = relates_to_events.remove(event.matrix_event_id()) {
+ if let Some(relates_to) = relates_to_events.remove(&event.matrix_event_id()) {
event.add_relates_to(
relates_to
.into_iter()
@@ -233,7 +235,7 @@ impl Timeline {
}
}
- if let Some(relates_to) = relates_to_events.remove(event.matrix_event_id()) {
+ if let Some(relates_to) = relates_to_events.remove(&event.matrix_event_id()) {
event.add_relates_to(
relates_to
.into_iter()
@@ -264,18 +266,28 @@ impl Timeline {
list.len()
};
+ let mut pending_events = priv_.pending_events.borrow_mut();
+
for event in batch.into_iter() {
let event_id = fn_event!(event, event_id).clone();
let user = self.room().member_by_id(fn_event!(event, sender));
- let event = Event::new(&event, &user);
-
- priv_.event_map.borrow_mut().insert(event_id, event.clone());
- if event.is_hidden_event() {
- self.add_hidden_event(event);
+ if let Some(pending_id) = pending_events.remove(&event_id) {
+ if let Some(event_obj) = priv_.event_map.borrow_mut().remove(&pending_id) {
+ event_obj.set_matrix_event(event);
+ priv_.event_map.borrow_mut().insert(event_id, event_obj);
+ }
added -= 1;
} else {
- priv_.list.borrow_mut().push_back(Item::for_event(event));
+ let event = Event::new(&event, &user);
+
+ priv_.event_map.borrow_mut().insert(event_id, event.clone());
+ if event.is_hidden_event() {
+ self.add_hidden_event(event);
+ added -= 1;
+ } else {
+ priv_.list.borrow_mut().push_back(Item::for_event(event));
+ }
}
}
@@ -285,6 +297,39 @@ impl Timeline {
self.items_changed(index as u32, 0, added as u32);
}
+ /// Append an event that wasn't yet fully send and received via a sync
+ pub fn append_pending(&self, event: AnyRoomEvent) {
+ let priv_ = imp::Timeline::from_instance(self);
+
+ let index = {
+ let mut list = priv_.list.borrow_mut();
+ let index = list.len();
+
+ let user = self.room().member_by_id(fn_event!(event, sender));
+ let event = Event::new(&event, &user);
+
+ if event.is_hidden_event() {
+ self.add_hidden_event(event);
+ None
+ } else {
+ list.push_back(Item::for_event(event));
+ Some(index)
+ }
+ };
+
+ if let Some(index) = index {
+ self.items_changed(index as u32, 0, 1);
+ }
+ }
+
+ pub fn set_event_id_for_pending(&self, pending_event_id: EventId, event_id: EventId) {
+ let priv_ = imp::Timeline::from_instance(self);
+ priv_
+ .pending_events
+ .borrow_mut()
+ .insert(event_id, pending_event_id);
+ }
+
/// Returns the event with the given id
pub fn event_by_id(&self, event_id: &EventId) -> Option<Event> {
// TODO: if the referenced event isn't known to us we will need to request it
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]