[fractal] room-history: Allow to send replies



commit e35c2b44644613654ffd0f2df890225a2188e90d
Author: Kévin Commaille <zecakeh tedomum fr>
Date:   Fri Aug 19 13:34:37 2022 +0200

    room-history: Allow to send replies

 Cargo.lock                                   |  24 +++
 Cargo.toml                                   |   7 +-
 data/resources/style.css                     |  22 ++-
 data/resources/ui/content-room-history.ui    | 194 ++++++++++++++--------
 data/resources/ui/event-menu.ui              |   4 +
 src/session/content/room_history/item_row.rs |   1 -
 src/session/content/room_history/mod.rs      | 237 +++++++++++++++++++++++++--
 src/session/room/event_actions.rs            |  18 +-
 8 files changed, 423 insertions(+), 84 deletions(-)
---
diff --git a/Cargo.lock b/Cargo.lock
index 6964af93d..35889f7a6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3100,7 +3100,9 @@ version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
 dependencies = [
+ "phf_macros",
  "phf_shared 0.10.0",
+ "proc-macro-hack",
 ]
 
 [[package]]
@@ -3143,6 +3145,20 @@ dependencies = [
  "rand 0.8.5",
 ]
 
+[[package]]
+name = "phf_macros"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0"
+dependencies = [
+ "phf_generator 0.10.0",
+ "phf_shared 0.10.0",
+ "proc-macro-hack",
+ "proc-macro2 1.0.43",
+ "quote 1.0.21",
+ "syn 1.0.99",
+]
+
 [[package]]
 name = "phf_shared"
 version = "0.8.0"
@@ -3328,6 +3344,12 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.19"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
+
 [[package]]
 name = "proc-macro2"
 version = "0.4.30"
@@ -3657,6 +3679,7 @@ dependencies = [
  "bytes",
  "form_urlencoded",
  "getrandom 0.2.7",
+ "html5ever 0.25.2",
  "http",
  "indexmap",
  "itoa",
@@ -3664,6 +3687,7 @@ dependencies = [
  "js_int",
  "js_option",
  "percent-encoding",
+ "phf 0.10.1",
  "pulldown-cmark",
  "rand 0.8.5",
  "regex",
diff --git a/Cargo.toml b/Cargo.toml
index 561777613..9a2f18e2a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -78,4 +78,9 @@ features = ["socks", "sso-login", "markdown", "qrcode", "experimental-timeline"]
 [dependencies.ruma]
 git = "https://github.com/ruma/ruma";
 rev = "c745d3baf720b38a254e640a526717864e87a065"
-features = ["unstable-pre-spec", "client-api-c"]
+features = [
+    "unstable-pre-spec",
+    "client-api-c",
+    "unstable-msc3440",
+    "unstable-sanitize",
+]
diff --git a/data/resources/style.css b/data/resources/style.css
index 9ec717352..b0c84e9d2 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -418,6 +418,10 @@ login {
   margin-right: 46px;
 }
 
+.room-history row.highlight {
+  background-color: alpha(@accent_bg_color, .1);
+}
+
 .room-history .event-content .emoji {
   font-size: 3em;
 }
@@ -438,7 +442,8 @@ login {
   padding: 2px 5px;
 }
 
-.room-history .event-content .quote {
+.room-history .event-content .quote,
+.related-event-content {
   border-left: 2px solid @accent_bg_color;
   padding-left: 6px;
   opacity: 0.7;
@@ -525,6 +530,21 @@ message-reactions .reaction-count {
   margin-bottom: 0px;
 }
 
+.related-event-toolbar {
+  padding: 0 6px 0 12px;
+}
+
+.related-event-toolbar button {
+  margin: 12px 6px;
+  min-height: 24px;
+  min-width: 24px;
+}
+
+.related-event-content {
+  padding-top: 2px;
+  padding-bottom: 2px;
+}
+
 
 /* Event Source Dialog */
 
diff --git a/data/resources/ui/content-room-history.ui b/data/resources/ui/content-room-history.ui
index 8be8f7541..9c681e203 100644
--- a/data/resources/ui/content-room-history.ui
+++ b/data/resources/ui/content-room-history.ui
@@ -229,83 +229,143 @@
             <property name="tightening-threshold">550</property>
             <child>
               <object class="GtkBox">
-                <style>
-                  <class name="toolbar"/>
-                </style>
-                <child>
-                  <object class="GtkMenuButton" id="markdown_button">
-                    <property name="valign">end</property>
-                    <property name="direction">up</property>
-                    <property name="icon-name">format-justify-left-symbolic</property>
-                    <property name="popover">
-                      <object class="MarkdownPopover">
-                        <property name="markdown-enabled" bind-source="ContentRoomHistory" 
bind-property="markdown-enabled" bind-flags="sync-create | bidirectional"/>
-                      </object>
-                    </property>
-                    <accessibility>
-                      <property name="label" translatable="yes">Enable Markdown Formatting</property>
-                    </accessibility>
-                  </object>
-                </child>
+                <property name="orientation">vertical</property>
                 <child>
-                  <object class="CustomEntry">
+                  <object class="GtkBox" id="related_event_toolbar">
+                    <style>
+                      <class name="related-event-toolbar"/>
+                    </style>
+                    <property name="spacing">12</property>
+                    <binding name="visible">
+                      <closure type="gboolean" function="object_is_some">
+                        <lookup name="related-event">ContentRoomHistory</lookup>
+                      </closure>
+                    </binding>
                     <child>
-                      <object class="GtkScrolledWindow">
-                        <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>
-                            <property name="accepts-tab">False</property>
-                            <property name="top-margin">7</property>
-                            <property name="bottom-margin">7</property>
-                            <property name="wrap-mode">word</property>
-                            <accessibility>
-                              <property name="label" translatable="yes">Message Entry</property>
-                            </accessibility>
+                      <object class="GtkBox">
+                        <property name="margin-bottom">6</property>
+                        <property name="margin-top">8</property>
+                        <property name="orientation">vertical</property>
+                        <child>
+                          <object class="LabelWithWidgets" id="related_event_header">
+                            <style>
+                              <class name="heading"/>
+                            </style>
+                            <property name="valign">center</property>
+                            <property name="hexpand">true</property>
+                            <property name="margin-top">2</property>
                           </object>
-                        </property>
+                        </child>
+                        <child>
+                          <object class="ContentMessageContent" id="related_event_content">
+                            <style>
+                              <class name="related-event-content"/>
+                              <class name="dim-label"/>
+                            </style>
+                            <property name="format">ellipsized</property>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkGestureClick">
+                            <signal name="pressed" handler="handle_related_event_click" swapped="yes"/>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkButton">
+                        <style>
+                          <class name="circular"/>
+                        </style>
+                        <property name="halign">end</property>
+                        <property name="valign">start</property>
+                        <property name="icon-name">window-close-symbolic</property>
+                        <property name="action-name">room-history.clear-related-event</property>
                       </object>
                     </child>
                   </object>
                 </child>
                 <child>
-                  <object class="GtkButton">
-                    <property name="valign">end</property>
-                    <property name="icon-name">emoji-people-symbolic</property>
-                    <property name="action-name">room-history.open-emoji</property>
-                    <accessibility>
-                      <property name="label" translatable="yes">Open Emoji Picker</property>
-                    </accessibility>
-                  </object>
-                </child>
-                <child>
-                  <object class="GtkMenuButton">
-                    <property name="valign">end</property>
-                    <property name="direction">up</property>
-                    <property name="icon-name">view-more-horizontal-symbolic</property>
-                    <property name="menu-model">message-menu-model</property>
-                    <accessibility>
-                      <property name="label" translatable="yes">Open Message Menu</property>
-                    </accessibility>
-                  </object>
-                </child>
-                <child>
-                  <object class="GtkButton">
-                    <property name="valign">end</property>
-                    <property name="icon-name">send-symbolic</property>
-                    <property name="focus-on-click">False</property>
-                    <property name="action-name">room-history.send-text-message</property>
+                  <object class="GtkBox">
                     <style>
-                      <class name="suggested-action"/>
-                      <class name="circular"/>
+                      <class name="toolbar"/>
                     </style>
-                    <accessibility>
-                      <property name="label" translatable="yes">Send Message</property>
-                    </accessibility>
+                    <child>
+                      <object class="GtkMenuButton" id="markdown_button">
+                        <property name="valign">end</property>
+                        <property name="direction">up</property>
+                        <property name="icon-name">format-justify-left-symbolic</property>
+                        <property name="popover">
+                          <object class="MarkdownPopover">
+                            <property name="markdown-enabled" bind-source="ContentRoomHistory" 
bind-property="markdown-enabled" bind-flags="sync-create | bidirectional"/>
+                          </object>
+                        </property>
+                        <accessibility>
+                          <property name="label" translatable="yes">Enable Markdown Formatting</property>
+                        </accessibility>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="CustomEntry">
+                        <child>
+                          <object class="GtkScrolledWindow">
+                            <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>
+                                <property name="accepts-tab">False</property>
+                                <property name="top-margin">7</property>
+                                <property name="bottom-margin">7</property>
+                                <property name="wrap-mode">word</property>
+                                <accessibility>
+                                  <property name="label" translatable="yes">Message Entry</property>
+                                </accessibility>
+                              </object>
+                            </property>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkButton">
+                        <property name="valign">end</property>
+                        <property name="icon-name">emoji-people-symbolic</property>
+                        <property name="action-name">room-history.open-emoji</property>
+                        <accessibility>
+                          <property name="label" translatable="yes">Open Emoji Picker</property>
+                        </accessibility>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkMenuButton">
+                        <property name="valign">end</property>
+                        <property name="direction">up</property>
+                        <property name="icon-name">view-more-horizontal-symbolic</property>
+                        <property name="menu-model">message-menu-model</property>
+                        <accessibility>
+                          <property name="label" translatable="yes">Open Message Menu</property>
+                        </accessibility>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkButton">
+                        <property name="valign">end</property>
+                        <property name="icon-name">send-symbolic</property>
+                        <property name="focus-on-click">False</property>
+                        <property name="action-name">room-history.send-text-message</property>
+                        <style>
+                          <class name="suggested-action"/>
+                          <class name="circular"/>
+                        </style>
+                        <accessibility>
+                          <property name="label" translatable="yes">Send Message</property>
+                        </accessibility>
+                      </object>
+                    </child>
                   </object>
                 </child>
               </object>
diff --git a/data/resources/ui/event-menu.ui b/data/resources/ui/event-menu.ui
index 3a8cb63c3..fdae72c88 100644
--- a/data/resources/ui/event-menu.ui
+++ b/data/resources/ui/event-menu.ui
@@ -8,16 +8,19 @@
     </section>
     <section>
       <item>
+        <!-- Translators: In this string, 'Reply' is a verb. -->
         <attribute name="label" translatable="yes">_Reply</attribute>
         <attribute name="action">event.reply</attribute>
         <attribute name="hidden-when">action-missing</attribute>
       </item>
       <item>
+        <!-- Translators: In this string, 'Edit' is a verb. -->
         <attribute name="label" translatable="yes">_Edit</attribute>
         <attribute name="action">event.edit</attribute>
         <attribute name="hidden-when">action-missing</attribute>
       </item>
       <item>
+        <!-- Translators: In this string, 'Forward' is a verb. -->
         <attribute name="label" translatable="yes">_Forward</attribute>
         <attribute name="action">event.forward</attribute>
         <attribute name="hidden-when">action-missing</attribute>
@@ -108,6 +111,7 @@
   <menu id="state_menu_model">
     <section>
       <item>
+        <!-- Translators: In this string, 'Forward' is a verb. -->
         <attribute name="label" translatable="yes">_Forward</attribute>
         <attribute name="action">event.forward</attribute>
         <attribute name="hidden-when">action-missing</attribute>
diff --git a/src/session/content/room_history/item_row.rs b/src/session/content/room_history/item_row.rs
index 1511cf5cb..67b1a20ba 100644
--- a/src/session/content/room_history/item_row.rs
+++ b/src/session/content/room_history/item_row.rs
@@ -5,7 +5,6 @@ use matrix_sdk::ruma::events::AnySyncTimelineEvent;
 
 use crate::{
     components::{ContextMenuBin, ContextMenuBinExt, ContextMenuBinImpl, ReactionChooser},
-    prelude::*,
     session::{
         content::room_history::{message_row::MessageRow, DividerRow, RoomHistory, StateRow},
         room::{
diff --git a/src/session/content/room_history/mod.rs b/src/session/content/room_history/mod.rs
index e4af34750..21203885a 100644
--- a/src/session/content/room_history/mod.rs
+++ b/src/session/content/room_history/mod.rs
@@ -17,24 +17,33 @@ use futures::TryFutureExt;
 use gettextrs::gettext;
 use gtk::{
     gdk, gio, glib,
-    glib::{clone, signal::Inhibit},
+    glib::{clone, signal::Inhibit, FromVariant},
     prelude::*,
     CompositeTemplate,
 };
 use log::{error, warn};
-use matrix_sdk::ruma::events::room::message::{
-    EmoteMessageEventContent, FormattedBody, MessageType, RoomMessageEventContent,
-    TextMessageEventContent,
+use matrix_sdk::ruma::{
+    events::{
+        room::message::{
+            EmoteMessageEventContent, FormattedBody, MessageType, TextMessageEventContent,
+        },
+        AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent,
+    },
+    EventId,
+};
+use ruma::events::{
+    room::message::{ForwardThread, LocationMessageEventContent, RoomMessageEventContent},
+    AnyMessageLikeEventContent,
 };
-use ruma::events::{room::message::LocationMessageEventContent, AnyMessageLikeEventContent};
 use sourceview::prelude::*;
 
 use self::{
     attachment_dialog::AttachmentDialog, completion::CompletionPopover, divider_row::DividerRow,
-    item_row::ItemRow, state_row::StateRow, verification_info_bar::VerificationInfoBar,
+    item_row::ItemRow, message_row::content::MessageContent, state_row::StateRow,
+    verification_info_bar::VerificationInfoBar,
 };
 use crate::{
-    components::{CustomEntry, DragOverlay, Pill, ReactionChooser, RoomTitle},
+    components::{CustomEntry, DragOverlay, LabelWithWidgets, Pill, ReactionChooser, RoomTitle},
     i18n::gettext_f,
     session::{
         content::{room_details, MarkdownPopover, RoomDetails},
@@ -42,9 +51,23 @@ use crate::{
         user::UserExt,
     },
     spawn, spawn_tokio, toast,
-    utils::filename_for_mime,
+    utils::{filename_for_mime, TemplateCallbacks},
 };
 
+#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
+#[repr(i32)]
+#[enum_type(name = "RelatedEventType")]
+pub enum RelatedEventType {
+    None = 0,
+    Reply = 1,
+}
+
+impl Default for RelatedEventType {
+    fn default() -> Self {
+        Self::None
+    }
+}
+
 mod imp {
     use std::cell::{Cell, RefCell};
 
@@ -98,6 +121,12 @@ mod imp {
         #[template_child]
         pub drag_overlay: TemplateChild<DragOverlay>,
         pub invite_action_watch: RefCell<Option<gtk::ExpressionWatch>>,
+        #[template_child]
+        pub related_event_header: TemplateChild<LabelWithWidgets>,
+        #[template_child]
+        pub related_event_content: TemplateChild<MessageContent>,
+        pub related_event_type: Cell<RelatedEventType>,
+        pub related_event: RefCell<Option<SupportedEvent>>,
     }
 
     #[glib::object_subclass]
@@ -113,6 +142,8 @@ mod imp {
             VerificationInfoBar::static_type();
             Timeline::static_type();
             Self::bind_template(klass);
+            Self::Type::bind_template_callbacks(klass);
+            TemplateCallbacks::bind_template_callbacks(klass);
             klass.set_accessible_role(gtk::AccessibleRole::Group);
             klass.install_action(
                 "room-history.send-text-message",
@@ -174,6 +205,27 @@ mod imp {
                     }
                 }));
             });
+
+            klass.install_action(
+                "room-history.clear-related-event",
+                None,
+                move |widget, _, _| widget.clear_related_event(),
+            );
+
+            klass.install_action("room-history.reply", Some("s"), move |widget, _, v| {
+                if let Some(event_id) = v
+                    .and_then(String::from_variant)
+                    .and_then(|s| EventId::parse(s).ok())
+                {
+                    if let Some(event) = widget
+                        .room()
+                        .and_then(|room| room.timeline().event_by_id(&event_id))
+                        .and_then(|event| event.downcast().ok())
+                    {
+                        widget.set_reply_to(event);
+                    }
+                }
+            });
         }
 
         fn instance_init(obj: &InitializingObject<Self>) {
@@ -221,6 +273,21 @@ mod imp {
                         true,
                         glib::ParamFlags::READWRITE,
                     ),
+                    glib::ParamSpecEnum::new(
+                        "related-event-type",
+                        "Related event type",
+                        "The type of related event of the composer",
+                        RelatedEventType::static_type(),
+                        RelatedEventType::default() as i32,
+                        glib::ParamFlags::READABLE,
+                    ),
+                    glib::ParamSpecObject::new(
+                        "related-event",
+                        "Related Event",
+                        "The related event of the composer",
+                        SupportedEvent::static_type(),
+                        glib::ParamFlags::READABLE,
+                    ),
                 ]
             });
 
@@ -264,6 +331,8 @@ mod imp {
                 "empty" => obj.room().is_none().to_value(),
                 "markdown-enabled" => self.md_enabled.get().to_value(),
                 "sticky" => obj.sticky().to_value(),
+                "related-event-type" => obj.related_event_type().to_value(),
+                "related-event" => obj.related_event().to_value(),
                 _ => unimplemented!(),
             }
         }
@@ -319,6 +388,12 @@ mod imp {
                 }
                 obj.start_loading();
             }));
+            adj.connect_page_size_notify(clone!(@weak obj => move |_| {
+                if obj.sticky() {
+                    obj.scroll_down();
+                }
+                obj.start_loading();
+            }));
 
             let key_events = gtk::EventControllerKey::new();
             self.message_entry.add_controller(&key_events);
@@ -349,9 +424,12 @@ mod imp {
 
             key_events
                 .connect_key_pressed(clone!(@weak obj => @default-return Inhibit(false), move |_, key, _, 
modifier| {
-                if !modifier.contains(gdk::ModifierType::SHIFT_MASK) && (key == gdk::Key::Return || key == 
gdk::Key::KP_Enter) {
+                if modifier.is_empty() && (key == gdk::Key::Return || key == gdk::Key::KP_Enter) {
                     obj.activate_action("room-history.send-text-message", None).unwrap();
                     Inhibit(true)
+                } else if modifier.is_empty() && key == gdk::Key::Escape && obj.related_event_type() != 
RelatedEventType::None {
+                    obj.clear_related_event();
+                    Inhibit(true)
                 } else {
                     Inhibit(false)
                 }
@@ -408,6 +486,7 @@ glib::wrapper! {
         @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
 }
 
+#[gtk::template_callbacks]
 impl RoomHistory {
     pub fn new() -> Self {
         glib::Object::new(&[]).expect("Failed to create RoomHistory")
@@ -436,6 +515,8 @@ impl RoomHistory {
             if let Some(invite_action) = priv_.invite_action_watch.take() {
                 invite_action.unwatch();
             }
+
+            self.clear_related_event();
         }
 
         if let Some(ref room) = room {
@@ -491,6 +572,64 @@ impl RoomHistory {
         self.imp().room.borrow().clone()
     }
 
+    pub fn related_event_type(&self) -> RelatedEventType {
+        self.imp().related_event_type.get()
+    }
+
+    fn set_related_event_type(&self, related_type: RelatedEventType) {
+        if self.related_event_type() == related_type {
+            return;
+        }
+
+        self.imp().related_event_type.set(related_type);
+        self.notify("related-event-type");
+    }
+
+    pub fn related_event(&self) -> Option<SupportedEvent> {
+        self.imp().related_event.borrow().clone()
+    }
+
+    fn set_related_event(&self, event: Option<SupportedEvent>) {
+        let prev_event = self.related_event();
+
+        if prev_event == event {
+            return;
+        }
+
+        if let Some(event) = &prev_event {
+            self.set_event_highlight(&event.event_id(), false);
+        }
+        if let Some(event) = &event {
+            self.set_event_highlight(&event.event_id(), true);
+        }
+
+        self.imp().related_event.replace(event);
+        self.notify("related-event");
+    }
+
+    pub fn clear_related_event(&self) {
+        self.set_related_event(None);
+        self.set_related_event_type(RelatedEventType::default());
+    }
+
+    pub fn set_reply_to(&self, event: SupportedEvent) {
+        let priv_ = self.imp();
+        priv_
+            .related_event_header
+            .set_widgets(vec![Pill::for_user(event.sender().upcast_ref())]);
+        priv_
+            .related_event_header
+            // Translators: Do NOT translate the content between '{' and '}',
+            // this is a variable name. In this string, 'Reply' is a noun.
+            .set_label(Some(gettext_f("Reply to {user}", &[("user", "<widget>")])));
+
+        priv_.related_event_content.update_for_event(&event);
+
+        self.set_related_event_type(RelatedEventType::Reply);
+        self.set_related_event(Some(event));
+        priv_.message_entry.grab_focus();
+    }
+
     /// Get an iterator over chunks of the message entry's text between the
     /// given start and end, split by mentions.
     fn split_buffer_mentions(&self, start: gtk::TextIter, end: gtk::TextIter) -> SplitMentions {
@@ -542,22 +681,47 @@ impl RoomHistory {
             None
         };
 
-        let content = RoomMessageEventContent::new(if is_emote {
+        let content = if is_emote {
             MessageType::Emote(if let Some(html_body) = html_body {
                 EmoteMessageEventContent::html(plain_body, html_body)
             } else {
                 EmoteMessageEventContent::plain(plain_body)
             })
+            .into()
         } else {
-            MessageType::Text(if let Some(html_body) = html_body {
+            let msg_type = MessageType::Text(if let Some(html_body) = html_body {
                 TextMessageEventContent::html(plain_body, html_body)
             } else {
                 TextMessageEventContent::plain(plain_body)
-            })
-        });
+            });
+
+            if self.related_event_type() == RelatedEventType::Reply {
+                // TODO: Use replacement event.
+                let related_event = self.related_event().unwrap().matrix_event();
+                if let AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
+                    SyncMessageLikeEvent::Original(related_message_event),
+                )) = related_event
+                {
+                    let full_related_message_event = related_message_event
+                        .into_full_event(self.room().unwrap().room_id().to_owned());
+                    RoomMessageEventContent::reply(
+                        msg_type,
+                        &full_related_message_event,
+                        ForwardThread::Yes,
+                    )
+                } else {
+                    // The message was redacted after being selected so ignore
+                    // the reply.
+                    msg_type.into()
+                }
+            } else {
+                msg_type.into()
+            }
+        };
 
         self.room().unwrap().send_room_message_event(content);
         buffer.set_text("");
+        self.clear_related_event();
     }
 
     pub fn leave(&self) {
@@ -992,6 +1156,53 @@ impl RoomHistory {
             self.clipboard().set_text(&content);
         }
     }
+
+    #[template_callback]
+    fn handle_related_event_click(&self, n_pressed: i32) {
+        if n_pressed == 1 {
+            if let Some(related_event) = &*self.imp().related_event.borrow() {
+                self.scroll_to_event(&related_event.event_id());
+            }
+        }
+    }
+
+    fn scroll_to_event(&self, event_id: &EventId) {
+        let room = match self.room() {
+            Some(room) => room,
+            None => return,
+        };
+
+        if let Some(pos) = room.timeline().find_event_position(event_id) {
+            let pos = pos as u32;
+            let _ = self
+                .imp()
+                .listview
+                .activate_action("list.scroll-to-item", Some(&pos.to_variant()));
+        }
+    }
+
+    fn set_event_highlight(&self, event_id: &EventId, highlight: bool) {
+        let mut child = self.imp().listview.first_child();
+        while let Some(widget) = child {
+            if widget
+                .first_child()
+                .and_then(|w| w.downcast::<ItemRow>().ok())
+                .and_then(|row| row.item())
+                .and_then(|item| item.downcast::<SupportedEvent>().ok())
+                .filter(|event| event.event_id() == event_id)
+                .is_some()
+            {
+                if highlight && !widget.has_css_class("highlight") {
+                    widget.add_css_class("highlight");
+                } else if !highlight && widget.has_css_class("highlight") {
+                    widget.remove_css_class("highlight");
+                }
+
+                break;
+            }
+            child = widget.next_sibling();
+        }
+    }
 }
 
 impl Default for RoomHistory {
diff --git a/src/session/room/event_actions.rs b/src/session/room/event_actions.rs
index 2e750c4a9..0e2fb165f 100644
--- a/src/session/room/event_actions.rs
+++ b/src/session/room/event_actions.rs
@@ -125,6 +125,8 @@ where
                     .map(|user| user.user_id())
                     .unwrap();
                 let user = event.room().members().member_by_id(user_id);
+
+                // Remove message
                 if event.sender() == user
                     || event
                         .room()
@@ -132,7 +134,6 @@ where
                         .min_level_for_room_action(&RoomAction::Redact)
                         <= user.power_level()
                 {
-                    // Remove message
                     gtk_macros::action!(
                         &action_group,
                         "remove",
@@ -141,6 +142,7 @@ where
                         })
                     );
                 }
+
                 // Send/redact a reaction
                 gtk_macros::action!(
                     &action_group,
@@ -161,6 +163,20 @@ where
                         }
                     })
                 );
+
+                // Reply
+                gtk_macros::action!(
+                    &action_group,
+                    "reply",
+                    None,
+                    clone!(@weak event, @weak self as widget => move |_, _| {
+                        let _ = widget.activate_action(
+                            "room-history.reply",
+                            Some(&event.event_id().as_str().to_variant())
+                        );
+                    })
+                );
+
                 match message.msgtype {
                     // Copy Text-Message
                     MessageType::Text(text_message) => {


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]