[fractal/fractal-next] content: Send room messages



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]