From d529e0d57675ccad1a29cfe5144ba95814460e4c Mon Sep 17 00:00:00 2001 From: Julian Sparber Date: Mon, 3 May 2021 18:09:05 +0200 Subject: [PATCH] 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 @@ vertical - + view-more-symbolic @@ -78,6 +78,7 @@ mail-attachment-symbolic + content.select-file @@ -86,13 +87,30 @@ - + + True True + external + 200 + True + + + True + + + send-symbolic + False + content.send-text-message + 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, pub room: RefCell>, + pub md_enabled: Cell, #[template_child] pub headerbar: TemplateChild, #[template_child] pub listview: TemplateChild, #[template_child] pub scrolled_window: TemplateChild, + #[template_child] + pub message_entry: TemplateChild, } #[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) { @@ -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, + pub event: OnceCell>, pub relates_to: RefCell>, pub show_header: Cell, pub sender: OnceCell, @@ -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::().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 { 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 { - 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 { 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 { 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, pub timeline: OnceCell, pub room_members: RefCell>, + /// The user of this room + pub user_id: OnceCell, } #[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>, /// A Hashmap linking `EventId` to correspondenting `Event` pub event_map: RefCell>, + /// Maps the temporary `EventId` of the pending Event to the real `EventId` + pub pending_events: RefCell>, } #[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 { // TODO: if the referenced event isn't known to us we will need to request it