[fractal] message-row: Allow to embed messages content preview



commit 8127a52199bf94edbff31629c58f76b3e3159953
Author: Kévin Commaille <zecakeh tedomum fr>
Date:   Fri Jul 15 12:42:11 2022 +0200

    message-row: Allow to embed messages content preview
    
    Provide a more compact format for message content.

 data/resources/ui/content-message-row.ui           |   2 +-
 po/POTFILES.in                                     |   2 +-
 src/components/label_with_widgets.rs               |  78 ++++-
 src/components/location_viewer.rs                  |  59 ++++
 .../content/room_history/message_row/audio.rs      |   5 +-
 .../content/room_history/message_row/content.rs    | 370 +++++++++++++++++++++
 .../content/room_history/message_row/file.rs       |   9 +
 .../content/room_history/message_row/location.rs   |  27 +-
 .../content/room_history/message_row/media.rs      |  10 +-
 .../content/room_history/message_row/mod.rs        | 275 ++-------------
 .../content/room_history/message_row/text.rs       | 129 +++++--
 11 files changed, 661 insertions(+), 305 deletions(-)
---
diff --git a/data/resources/ui/content-message-row.ui b/data/resources/ui/content-message-row.ui
index 17c679553..05addb775 100644
--- a/data/resources/ui/content-message-row.ui
+++ b/data/resources/ui/content-message-row.ui
@@ -41,7 +41,7 @@
               </object>
             </child>
             <child>
-              <object class="AdwBin" id="content">
+              <object class="ContentMessageContent" id="content">
                 <property name="hexpand">True</property>
                 <property name="vexpand">True</property>
                 <style>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index b58010e62..41468cb53 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -62,8 +62,8 @@ src/session/content/room_details/member_page/mod.rs
 src/session/content/room_details/mod.rs
 src/session/content/room_history/item_row.rs
 src/session/content/room_history/message_row/audio.rs
+src/session/content/room_history/message_row/content.rs
 src/session/content/room_history/message_row/media.rs
-src/session/content/room_history/message_row/mod.rs
 src/session/content/room_history/mod.rs
 src/session/content/room_history/state_row/creation.rs
 src/session/content/room_history/state_row/mod.rs
diff --git a/src/components/label_with_widgets.rs b/src/components/label_with_widgets.rs
index b13dd95e2..4052f4a99 100644
--- a/src/components/label_with_widgets.rs
+++ b/src/components/label_with_widgets.rs
@@ -10,7 +10,7 @@ fn pango_pixels(d: i32) -> i32 {
 }
 
 mod imp {
-    use std::cell::RefCell;
+    use std::cell::{Cell, RefCell};
 
     use super::*;
 
@@ -21,6 +21,7 @@ mod imp {
         pub label: gtk::Label,
         pub placeholder: RefCell<Option<String>>,
         pub text: RefCell<Option<String>>,
+        pub ellipsize: Cell<bool>,
     }
 
     #[glib::object_subclass]
@@ -57,6 +58,13 @@ mod imp {
                         None,
                         glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
                     ),
+                    glib::ParamSpecString::new(
+                        "ellipsize",
+                        "Ellipsize",
+                        "Whether the label's text should be ellipsized.",
+                        None,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
                 ]
             });
 
@@ -74,6 +82,7 @@ mod imp {
                 "label" => obj.set_label(value.get().unwrap()),
                 "placeholder" => obj.set_placeholder(value.get().unwrap()),
                 "use-markup" => obj.set_use_markup(value.get().unwrap()),
+                "ellipsize" => obj.set_ellipsize(value.get().unwrap()),
                 _ => unimplemented!(),
             }
         }
@@ -83,6 +92,7 @@ mod imp {
                 "label" => obj.label().to_value(),
                 "placeholder" => obj.placeholder().to_value(),
                 "use-markup" => obj.uses_markup().to_value(),
+                "ellipsize" => obj.ellipsize().to_value(),
                 _ => unimplemented!(),
             }
         }
@@ -234,15 +244,8 @@ impl LabelWithWidgets {
             return;
         }
 
-        if let Some(ref label) = label {
-            let placeholder = priv_.placeholder.borrow();
-            let placeholder = placeholder.as_deref().unwrap_or(DEFAULT_PLACEHOLDER);
-            let label = label.replace(placeholder, OBJECT_REPLACEMENT_CHARACTER);
-            priv_.label.set_label(&label);
-        }
-
         priv_.text.replace(label);
-        self.invalidate_child_widgets();
+        self.update_label();
         self.notify("label");
     }
 
@@ -257,14 +260,8 @@ impl LabelWithWidgets {
             return;
         }
 
-        if let Some(text) = &*priv_.text.borrow() {
-            let placeholder = placeholder.as_deref().unwrap_or(DEFAULT_PLACEHOLDER);
-            let label = text.replace(placeholder, OBJECT_REPLACEMENT_CHARACTER);
-            priv_.label.set_text(&label);
-        }
-
         priv_.placeholder.replace(placeholder);
-        self.invalidate_child_widgets();
+        self.update_label();
         self.notify("placeholder");
     }
 
@@ -376,6 +373,55 @@ impl LabelWithWidgets {
     pub fn set_use_markup(&self, use_markup: bool) {
         self.imp().label.set_use_markup(use_markup);
     }
+
+    /// Whether the text of the label is ellipsized.
+    pub fn ellipsize(&self) -> bool {
+        self.imp().ellipsize.get()
+    }
+
+    /// Sets whether the text of the label should be ellipsized.
+    pub fn set_ellipsize(&self, ellipsize: bool) {
+        if self.ellipsize() == ellipsize {
+            return;
+        }
+
+        self.imp().ellipsize.set(true);
+        self.update_label();
+        self.notify("ellipsize");
+    }
+
+    fn update_label(&self) {
+        let priv_ = self.imp();
+        if self.ellipsize() {
+            // Workaround: if both wrap and ellipsize are set, and there are
+            // widgets inserted, GtkLabel reports an erroneous minimum width.
+            priv_.label.set_wrap(false);
+            priv_.label.set_ellipsize(pango::EllipsizeMode::End);
+
+            if let Some(label) = priv_.text.borrow().as_ref() {
+                let placeholder = priv_.placeholder.borrow();
+                let placeholder = placeholder.as_deref().unwrap_or(DEFAULT_PLACEHOLDER);
+                let label = label.replace(placeholder, OBJECT_REPLACEMENT_CHARACTER);
+                let label = if let Some(pos) = label.find('\n') {
+                    format!("{}…", &label[0..pos])
+                } else {
+                    label
+                };
+                priv_.label.set_label(&label);
+            }
+        } else {
+            priv_.label.set_wrap(true);
+            priv_.label.set_ellipsize(pango::EllipsizeMode::None);
+
+            if let Some(label) = priv_.text.borrow().as_ref() {
+                let placeholder = priv_.placeholder.borrow();
+                let placeholder = placeholder.as_deref().unwrap_or(DEFAULT_PLACEHOLDER);
+                let label = label.replace(placeholder, OBJECT_REPLACEMENT_CHARACTER);
+                priv_.label.set_label(&label);
+            }
+        }
+        self.invalidate_child_widgets();
+    }
 }
 
 impl Default for LabelWithWidgets {
diff --git a/src/components/location_viewer.rs b/src/components/location_viewer.rs
index ce7468490..4159f8f61 100644
--- a/src/components/location_viewer.rs
+++ b/src/components/location_viewer.rs
@@ -5,7 +5,10 @@ use shumate::prelude::*;
 use crate::i18n::gettext_f;
 
 mod imp {
+    use std::cell::Cell;
+
     use glib::subclass::InitializingObject;
+    use once_cell::sync::Lazy;
 
     use super::*;
 
@@ -17,6 +20,7 @@ mod imp {
         #[template_child]
         pub marker_img: TemplateChild<gtk::Image>,
         pub marker: shumate::Marker,
+        pub compact: Cell<bool>,
     }
 
     #[glib::object_subclass]
@@ -36,6 +40,40 @@ mod imp {
     }
 
     impl ObjectImpl for LocationViewer {
+        fn properties() -> &'static [glib::ParamSpec] {
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![glib::ParamSpecBoolean::new(
+                    "compact",
+                    "Compact",
+                    "Whether to display this location in a compact format",
+                    false,
+                    glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                )]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn set_property(
+            &self,
+            obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.name() {
+                "compact" => obj.set_compact(value.get().unwrap()),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "compact" => obj.compact().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+
         fn constructed(&self, obj: &Self::Type) {
             self.marker.set_child(Some(&*self.marker_img));
 
@@ -72,6 +110,27 @@ impl LocationViewer {
         glib::Object::new(&[]).expect("Failed to create LocationViewer")
     }
 
+    /// Whether to display this location in a compact format.
+    pub fn compact(&self) -> bool {
+        self.imp().compact.get()
+    }
+
+    /// Set the compact format of this location.
+    pub fn set_compact(&self, compact: bool) {
+        if self.compact() == compact {
+            return;
+        }
+
+        let map = &self.imp().map;
+        map.set_show_zoom_buttons(!compact);
+        if let Some(license) = map.license() {
+            license.set_visible(!compact);
+        }
+
+        self.imp().compact.set(compact);
+        self.notify("compact");
+    }
+
     pub fn set_geo_uri(&self, uri: &str) {
         let imp = self.imp();
 
diff --git a/src/session/content/room_history/message_row/audio.rs 
b/src/session/content/room_history/message_row/audio.rs
index 9069765fd..64bd32d24 100644
--- a/src/session/content/room_history/message_row/audio.rs
+++ b/src/session/content/room_history/message_row/audio.rs
@@ -8,7 +8,7 @@ use gtk::{
 use log::warn;
 use matrix_sdk::{media::MediaEventContent, ruma::events::room::message::AudioMessageEventContent};
 
-use super::media::MediaState;
+use super::{media::MediaState, ContentFormat};
 use crate::{components::AudioPlayer, session::Session, spawn, spawn_tokio, utils::media_type_uid};
 
 mod imp {
@@ -198,9 +198,10 @@ impl MessageAudio {
     }
 
     /// Display the given `audio` message.
-    pub fn audio(&self, audio: AudioMessageEventContent, session: &Session, compact: bool) {
+    pub fn audio(&self, audio: AudioMessageEventContent, session: &Session, format: ContentFormat) {
         self.set_body(Some(audio.body.clone()));
 
+        let compact = matches!(format, ContentFormat::Compact | ContentFormat::Ellipsized);
         self.set_compact(compact);
         if compact {
             self.set_state(MediaState::Ready);
diff --git a/src/session/content/room_history/message_row/content.rs 
b/src/session/content/room_history/message_row/content.rs
new file mode 100644
index 000000000..48e7f8215
--- /dev/null
+++ b/src/session/content/room_history/message_row/content.rs
@@ -0,0 +1,370 @@
+use adw::{prelude::*, subclass::prelude::*};
+use gettextrs::gettext;
+use gtk::{glib, glib::clone};
+use log::warn;
+use matrix_sdk::ruma::events::{
+    room::message::{MessageType, Relation},
+    AnyMessageLikeEventContent,
+};
+
+use super::{
+    audio::MessageAudio, file::MessageFile, location::MessageLocation, media::MessageMedia,
+    reply::MessageReply, text::MessageText,
+};
+use crate::{prelude::*, session::room::SupportedEvent, spawn, utils::filename_for_mime};
+
+#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
+#[repr(i32)]
+#[enum_type(name = "ContentFormat")]
+pub enum ContentFormat {
+    /// The content should appear at its natural size.
+    Natural = 0,
+
+    /// The content should appear in a smaller format without interactions, if
+    /// possible.
+    ///
+    /// This has no effect on text replies.
+    ///
+    /// The related events of replies are not displayed.
+    Compact = 1,
+
+    /// Like `Compact`, but the content should be ellipsized if possible to show
+    /// only a single line.
+    Ellipsized = 2,
+}
+
+impl Default for ContentFormat {
+    fn default() -> Self {
+        Self::Natural
+    }
+}
+
+mod imp {
+    use std::cell::Cell;
+
+    use once_cell::sync::Lazy;
+
+    use super::*;
+
+    #[derive(Debug, Default)]
+    pub struct MessageContent {
+        pub format: Cell<ContentFormat>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for MessageContent {
+        const NAME: &'static str = "ContentMessageContent";
+        type Type = super::MessageContent;
+        type ParentType = adw::Bin;
+    }
+
+    impl ObjectImpl for MessageContent {
+        fn properties() -> &'static [glib::ParamSpec] {
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![glib::ParamSpecEnum::new(
+                    "format",
+                    "Format",
+                    "The displayed format of the message",
+                    ContentFormat::static_type(),
+                    ContentFormat::default() as i32,
+                    glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                )]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn set_property(
+            &self,
+            obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.name() {
+                "format" => obj.set_format(value.get().unwrap()),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "format" => obj.format().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+    }
+
+    impl WidgetImpl for MessageContent {}
+    impl BinImpl for MessageContent {}
+}
+
+glib::wrapper! {
+    pub struct MessageContent(ObjectSubclass<imp::MessageContent>)
+        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+impl MessageContent {
+    pub fn new() -> Self {
+        glib::Object::new(&[]).expect("Failed to create MessageContent")
+    }
+
+    pub fn format(&self) -> ContentFormat {
+        self.imp().format.get()
+    }
+
+    pub fn set_format(&self, format: ContentFormat) {
+        if self.format() == format {
+            return;
+        }
+
+        self.imp().format.set(format);
+        self.notify("format");
+    }
+
+    pub fn update_for_event(&self, event: &SupportedEvent) {
+        let format = self.format();
+        if format == ContentFormat::Natural && event.is_reply() {
+            spawn!(
+                glib::PRIORITY_HIGH,
+                clone!(@weak self as obj, @weak event => async move {
+                    if let Some(related_event) = event
+                        .reply_to_event()
+                        .await
+                        .ok()
+                        .flatten()
+                        .and_then(|event| event.downcast::<SupportedEvent>().ok())
+                    {
+                        let reply = MessageReply::new();
+                        reply.set_related_content_sender(related_event.sender().upcast());
+                        build_content(reply.related_content(), &related_event, ContentFormat::Compact);
+                        build_content(reply.content(), &event, ContentFormat::Natural);
+                        obj.set_child(Some(&reply));
+                    } else {
+                        build_content(&obj, &event, format);
+                    }
+                })
+            );
+        } else {
+            build_content(self, event, format);
+        }
+    }
+}
+
+impl Default for MessageContent {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+/// Build the content widget of `event` as a child of `parent`.
+fn build_content(parent: &impl IsA<adw::Bin>, event: &SupportedEvent, format: ContentFormat) {
+    let parent = parent.upcast_ref();
+    match event.content() {
+        Some(AnyMessageLikeEventContent::RoomMessage(message)) => {
+            let msgtype = if let Some(Relation::Replacement(replacement)) = message.relates_to {
+                replacement.new_content.msgtype
+            } else {
+                message.msgtype
+            };
+            match msgtype {
+                MessageType::Audio(message) => {
+                    let child = if let Some(Ok(child)) =
+                        parent.child().map(|w| w.downcast::<MessageAudio>())
+                    {
+                        child
+                    } else {
+                        let child = MessageAudio::new();
+                        parent.set_child(Some(&child));
+                        child
+                    };
+                    child.audio(message, &event.room().session(), format);
+                }
+                MessageType::Emote(message) => {
+                    let child = if let Some(Ok(child)) =
+                        parent.child().map(|w| w.downcast::<MessageText>())
+                    {
+                        child
+                    } else {
+                        let child = MessageText::new();
+                        parent.set_child(Some(&child));
+                        child
+                    };
+                    child.emote(
+                        message.formatted,
+                        message.body,
+                        event.sender(),
+                        &event.room(),
+                        format,
+                    );
+                }
+                MessageType::File(message) => {
+                    let info = message.info.as_ref();
+                    let filename = message
+                        .filename
+                        .filter(|name| !name.is_empty())
+                        .or(Some(message.body))
+                        .filter(|name| !name.is_empty())
+                        .unwrap_or_else(|| {
+                            filename_for_mime(info.and_then(|info| info.mimetype.as_deref()), None)
+                        });
+
+                    let child = if let Some(Ok(child)) =
+                        parent.child().map(|w| w.downcast::<MessageFile>())
+                    {
+                        child
+                    } else {
+                        let child = MessageFile::new();
+                        parent.set_child(Some(&child));
+                        child
+                    };
+                    child.set_filename(Some(filename));
+                    child.set_format(format);
+                }
+                MessageType::Image(message) => {
+                    let child = if let Some(Ok(child)) =
+                        parent.child().map(|w| w.downcast::<MessageMedia>())
+                    {
+                        child
+                    } else {
+                        let child = MessageMedia::new();
+                        parent.set_child(Some(&child));
+                        child
+                    };
+                    child.image(message, &event.room().session(), format);
+                }
+                MessageType::Location(message) => {
+                    let child = if let Some(Ok(child)) =
+                        parent.child().map(|w| w.downcast::<MessageLocation>())
+                    {
+                        child
+                    } else {
+                        let child = MessageLocation::new();
+                        parent.set_child(Some(&child));
+                        child
+                    };
+                    child.set_geo_uri(&message.geo_uri, format);
+                }
+                MessageType::Notice(message) => {
+                    let child = if let Some(Ok(child)) =
+                        parent.child().map(|w| w.downcast::<MessageText>())
+                    {
+                        child
+                    } else {
+                        let child = MessageText::new();
+                        parent.set_child(Some(&child));
+                        child
+                    };
+                    child.markup(message.formatted, message.body, &event.room(), format);
+                }
+                MessageType::ServerNotice(message) => {
+                    let child = if let Some(Ok(child)) =
+                        parent.child().map(|w| w.downcast::<MessageText>())
+                    {
+                        child
+                    } else {
+                        let child = MessageText::new();
+                        parent.set_child(Some(&child));
+                        child
+                    };
+                    child.text(message.body, format);
+                }
+                MessageType::Text(message) => {
+                    let child = if let Some(Ok(child)) =
+                        parent.child().map(|w| w.downcast::<MessageText>())
+                    {
+                        child
+                    } else {
+                        let child = MessageText::new();
+                        parent.set_child(Some(&child));
+                        child
+                    };
+                    child.markup(message.formatted, message.body, &event.room(), format);
+                }
+                MessageType::Video(message) => {
+                    let child = if let Some(Ok(child)) =
+                        parent.child().map(|w| w.downcast::<MessageMedia>())
+                    {
+                        child
+                    } else {
+                        let child = MessageMedia::new();
+                        parent.set_child(Some(&child));
+                        child
+                    };
+                    child.video(message, &event.room().session(), format);
+                }
+                MessageType::VerificationRequest(_) => {
+                    // TODO: show more information about the verification
+                    let child = if let Some(Ok(child)) =
+                        parent.child().map(|w| w.downcast::<MessageText>())
+                    {
+                        child
+                    } else {
+                        let child = MessageText::new();
+                        parent.set_child(Some(&child));
+                        child
+                    };
+                    child.text(gettext("Identity verification was started"), format);
+                }
+                _ => {
+                    warn!("Event not supported: {:?}", msgtype);
+                    let child = if let Some(Ok(child)) =
+                        parent.child().map(|w| w.downcast::<MessageText>())
+                    {
+                        child
+                    } else {
+                        let child = MessageText::new();
+                        parent.set_child(Some(&child));
+                        child
+                    };
+                    child.text(gettext("Unsupported event"), format);
+                }
+            }
+        }
+        Some(AnyMessageLikeEventContent::Sticker(content)) => {
+            let child =
+                if let Some(Ok(child)) = parent.child().map(|w| w.downcast::<MessageMedia>()) {
+                    child
+                } else {
+                    let child = MessageMedia::new();
+                    parent.set_child(Some(&child));
+                    child
+                };
+            child.sticker(content, &event.room().session(), format);
+        }
+        Some(AnyMessageLikeEventContent::RoomEncrypted(_)) => {
+            let child = if let Some(Ok(child)) = parent.child().map(|w| w.downcast::<MessageText>())
+            {
+                child
+            } else {
+                let child = MessageText::new();
+                parent.set_child(Some(&child));
+                child
+            };
+            child.text(gettext("Unable to decrypt this message, decryption will be retried once the keys are 
available."), format);
+        }
+        Some(AnyMessageLikeEventContent::RoomRedaction(_)) => {
+            let child = if let Some(Ok(child)) = parent.child().map(|w| w.downcast::<MessageText>())
+            {
+                child
+            } else {
+                let child = MessageText::new();
+                parent.set_child(Some(&child));
+                child
+            };
+            child.text(gettext("This message was removed."), format);
+        }
+        _ => {
+            warn!("Unsupported event: {:?}", event.content());
+            let child = if let Some(Ok(child)) = parent.child().map(|w| w.downcast::<MessageText>())
+            {
+                child
+            } else {
+                let child = MessageText::new();
+                parent.set_child(Some(&child));
+                child
+            };
+            child.text(gettext("Unsupported event"), format);
+        }
+    }
+}
diff --git a/src/session/content/room_history/message_row/file.rs 
b/src/session/content/room_history/message_row/file.rs
index b27110f37..75d6b3390 100644
--- a/src/session/content/room_history/message_row/file.rs
+++ b/src/session/content/room_history/message_row/file.rs
@@ -1,6 +1,8 @@
 use adw::subclass::prelude::*;
 use gtk::{glib, prelude::*, CompositeTemplate};
 
+use super::ContentFormat;
+
 mod imp {
     use std::cell::{Cell, RefCell};
 
@@ -125,6 +127,13 @@ impl MessageFile {
     pub fn compact(&self) -> bool {
         self.imp().compact.get()
     }
+
+    pub fn set_format(&self, format: ContentFormat) {
+        self.set_compact(matches!(
+            format,
+            ContentFormat::Compact | ContentFormat::Ellipsized
+        ));
+    }
 }
 
 impl Default for MessageFile {
diff --git a/src/session/content/room_history/message_row/location.rs 
b/src/session/content/room_history/message_row/location.rs
index 12dc03cfe..ab903ce78 100644
--- a/src/session/content/room_history/message_row/location.rs
+++ b/src/session/content/room_history/message_row/location.rs
@@ -1,6 +1,7 @@
 use adw::{prelude::*, subclass::prelude::*};
 use gtk::{glib, CompositeTemplate};
 
+use super::ContentFormat;
 use crate::components::LocationViewer;
 
 mod imp {
@@ -40,13 +41,26 @@ mod imp {
         fn measure(
             &self,
             _widget: &Self::Type,
-            _orientation: gtk::Orientation,
+            orientation: gtk::Orientation,
             _for_size: i32,
         ) -> (i32, i32, i32, i32) {
-            (300, 300, -1, -1)
+            if self.location.compact() {
+                if orientation == gtk::Orientation::Horizontal {
+                    (75, 75, -1, -1)
+                } else {
+                    (50, 50, -1, -1)
+                }
+            } else {
+                (300, 300, -1, -1)
+            }
         }
 
         fn size_allocate(&self, _widget: &Self::Type, width: i32, height: i32, baseline: i32) {
+            let width = if self.location.compact() {
+                width.min(75)
+            } else {
+                width
+            };
             self.location
                 .size_allocate(&gtk::Allocation::new(0, 0, width, height), baseline)
         }
@@ -66,7 +80,12 @@ impl MessageLocation {
         glib::Object::new(&[]).expect("Failed to create MessageLocation")
     }
 
-    pub fn set_geo_uri(&self, uri: &str) {
-        self.imp().location.set_geo_uri(uri);
+    pub fn set_geo_uri(&self, uri: &str, format: ContentFormat) {
+        let location = &self.imp().location;
+        location.set_geo_uri(uri);
+        location.set_compact(matches!(
+            format,
+            ContentFormat::Compact | ContentFormat::Ellipsized
+        ))
     }
 }
diff --git a/src/session/content/room_history/message_row/media.rs 
b/src/session/content/room_history/message_row/media.rs
index cc7abf286..a5d854345 100644
--- a/src/session/content/room_history/message_row/media.rs
+++ b/src/session/content/room_history/message_row/media.rs
@@ -18,6 +18,7 @@ use matrix_sdk::{
     },
 };
 
+use super::ContentFormat;
 use crate::{
     components::VideoPlayer,
     session::Session,
@@ -336,10 +337,11 @@ impl MessageMedia {
     }
 
     /// Display the given `image`, in a `compact` format or not.
-    pub fn image(&self, image: ImageMessageEventContent, session: &Session, compact: bool) {
+    pub fn image(&self, image: ImageMessageEventContent, session: &Session, format: ContentFormat) {
         let info = image.info.as_deref();
         let width = uint_to_i32(info.and_then(|info| info.width));
         let height = uint_to_i32(info.and_then(|info| info.height));
+        let compact = matches!(format, ContentFormat::Compact | ContentFormat::Ellipsized);
 
         self.set_width(width);
         self.set_height(height);
@@ -348,11 +350,12 @@ impl MessageMedia {
     }
 
     /// Display the given `sticker`, in a `compact` format or not.
-    pub fn sticker(&self, sticker: StickerEventContent, session: &Session, compact: bool) {
+    pub fn sticker(&self, sticker: StickerEventContent, session: &Session, format: ContentFormat) {
         let info = &sticker.info;
         let width = uint_to_i32(info.width);
         let height = uint_to_i32(info.height);
         let body = Some(sticker.body.clone());
+        let compact = matches!(format, ContentFormat::Compact | ContentFormat::Ellipsized);
 
         self.set_width(width);
         self.set_height(height);
@@ -361,11 +364,12 @@ impl MessageMedia {
     }
 
     /// Display the given `video`, in a `compact` format or not.
-    pub fn video(&self, video: VideoMessageEventContent, session: &Session, compact: bool) {
+    pub fn video(&self, video: VideoMessageEventContent, session: &Session, format: ContentFormat) {
         let info = &video.info.as_deref();
         let width = uint_to_i32(info.and_then(|info| info.width));
         let height = uint_to_i32(info.and_then(|info| info.height));
         let body = Some(video.body.clone());
+        let compact = matches!(format, ContentFormat::Compact | ContentFormat::Ellipsized);
 
         self.set_width(width);
         self.set_height(height);
diff --git a/src/session/content/room_history/message_row/mod.rs 
b/src/session/content/room_history/message_row/mod.rs
index 85b925b6e..23c2e8c6f 100644
--- a/src/session/content/room_history/message_row/mod.rs
+++ b/src/session/content/room_history/message_row/mod.rs
@@ -1,4 +1,5 @@
 mod audio;
+pub mod content;
 mod file;
 mod location;
 mod media;
@@ -8,25 +9,15 @@ mod reply;
 mod text;
 
 use adw::{prelude::*, subclass::prelude::*};
-use gettextrs::gettext;
 use gtk::{
     glib,
     glib::{clone, signal::SignalHandlerId},
     CompositeTemplate,
 };
-use log::warn;
-use matrix_sdk::ruma::events::{
-    room::message::{MessageType, Relation},
-    AnyMessageLikeEventContent,
-};
 
-use self::{
-    audio::MessageAudio, file::MessageFile, location::MessageLocation, media::MessageMedia,
-    reaction_list::MessageReactionList, reply::MessageReply, text::MessageText,
-};
-use crate::{
-    components::Avatar, prelude::*, session::room::SupportedEvent, spawn, utils::filename_for_mime,
-};
+pub use self::content::ContentFormat;
+use self::{content::MessageContent, reaction_list::MessageReactionList};
+use crate::{components::Avatar, prelude::*, session::room::SupportedEvent};
 
 mod imp {
     use std::cell::RefCell;
@@ -48,7 +39,7 @@ mod imp {
         #[template_child]
         pub timestamp: TemplateChild<gtk::Label>,
         #[template_child]
-        pub content: TemplateChild<adw::Bin>,
+        pub content: TemplateChild<MessageContent>,
         #[template_child]
         pub reactions: TemplateChild<MessageReactionList>,
         pub source_changed_handler: RefCell<Option<SignalHandlerId>>,
@@ -108,7 +99,20 @@ mod imp {
                 _ => unimplemented!(),
             }
         }
+
+        fn constructed(&self, obj: &Self::Type) {
+            self.content.connect_notify_local(
+                Some("format"),
+                clone!(@weak obj => move |content, _|
+                    obj.imp().reactions.set_visible(!matches!(
+                        content.format(),
+                        ContentFormat::Compact | ContentFormat::Ellipsized
+                    ));
+                ),
+            );
+        }
     }
+
     impl WidgetImpl for MessageRow {}
     impl BinImpl for MessageRow {}
 }
@@ -144,6 +148,10 @@ impl MessageRow {
         self.notify("show-header");
     }
 
+    pub fn set_content_format(&self, format: ContentFormat) {
+        self.imp().content.set_format(format);
+    }
+
     pub fn set_event(&self, event: SupportedEvent) {
         let priv_ = self.imp();
         // Remove signals and bindings from the previous event
@@ -196,32 +204,7 @@ impl MessageRow {
     }
 
     fn update_content(&self, event: &SupportedEvent) {
-        if event.is_reply() {
-            spawn!(
-                glib::PRIORITY_HIGH,
-                clone!(@weak self as obj, @weak event => async move {
-                    let priv_ = obj.imp();
-
-                    if let Some(related_event) = event
-                        .reply_to_event()
-                        .await
-                        .ok()
-                        .flatten()
-                        .and_then(|event| event.downcast::<SupportedEvent>().ok())
-                    {
-                        let reply = MessageReply::new();
-                        reply.set_related_content_sender(related_event.sender().upcast());
-                        build_content(reply.related_content(), &related_event, true);
-                        build_content(reply.content(), &event, false);
-                        priv_.content.set_child(Some(&reply));
-                    } else {
-                        build_content(&priv_.content, &event, false);
-                    }
-                })
-            );
-        } else {
-            build_content(&self.imp().content, event, false);
-        }
+        self.imp().content.update_for_event(event);
     }
 }
 
@@ -230,215 +213,3 @@ impl Default for MessageRow {
         Self::new()
     }
 }
-
-/// Build the content widget of `event` as a child of `parent`.
-///
-/// If `compact` is true, the content should appear in a smaller format without
-/// interactions, if possible.
-fn build_content(parent: &adw::Bin, event: &SupportedEvent, compact: bool) {
-    match event.content() {
-        Some(AnyMessageLikeEventContent::RoomMessage(message)) => {
-            let msgtype = if let Some(Relation::Replacement(replacement)) = message.relates_to {
-                replacement.new_content.msgtype
-            } else {
-                message.msgtype
-            };
-            match msgtype {
-                MessageType::Audio(message) => {
-                    let child = if let Some(Ok(child)) =
-                        parent.child().map(|w| w.downcast::<MessageAudio>())
-                    {
-                        child
-                    } else {
-                        let child = MessageAudio::new();
-                        parent.set_child(Some(&child));
-                        child
-                    };
-                    child.audio(message, &event.room().session(), compact);
-                }
-                MessageType::Emote(message) => {
-                    let child = if let Some(Ok(child)) =
-                        parent.child().map(|w| w.downcast::<MessageText>())
-                    {
-                        child
-                    } else {
-                        let child = MessageText::new();
-                        parent.set_child(Some(&child));
-                        child
-                    };
-                    child.emote(
-                        message.formatted,
-                        message.body,
-                        event.sender(),
-                        &event.room(),
-                    );
-                }
-                MessageType::File(message) => {
-                    let info = message.info.as_ref();
-                    let filename = message
-                        .filename
-                        .filter(|name| !name.is_empty())
-                        .or(Some(message.body))
-                        .filter(|name| !name.is_empty())
-                        .unwrap_or_else(|| {
-                            filename_for_mime(info.and_then(|info| info.mimetype.as_deref()), None)
-                        });
-
-                    let child = if let Some(Ok(child)) =
-                        parent.child().map(|w| w.downcast::<MessageFile>())
-                    {
-                        child
-                    } else {
-                        let child = MessageFile::new();
-                        parent.set_child(Some(&child));
-                        child
-                    };
-                    child.set_filename(Some(filename));
-                    child.set_compact(compact);
-                }
-                MessageType::Image(message) => {
-                    let child = if let Some(Ok(child)) =
-                        parent.child().map(|w| w.downcast::<MessageMedia>())
-                    {
-                        child
-                    } else {
-                        let child = MessageMedia::new();
-                        parent.set_child(Some(&child));
-                        child
-                    };
-                    child.image(message, &event.room().session(), compact);
-                }
-                MessageType::Location(message) => {
-                    let child = if let Some(Ok(child)) =
-                        parent.child().map(|w| w.downcast::<MessageLocation>())
-                    {
-                        child
-                    } else {
-                        let child = MessageLocation::new();
-                        parent.set_child(Some(&child));
-                        child
-                    };
-                    child.set_geo_uri(&message.geo_uri);
-                }
-                MessageType::Notice(message) => {
-                    let child = if let Some(Ok(child)) =
-                        parent.child().map(|w| w.downcast::<MessageText>())
-                    {
-                        child
-                    } else {
-                        let child = MessageText::new();
-                        parent.set_child(Some(&child));
-                        child
-                    };
-                    child.markup(message.formatted, message.body, &event.room());
-                }
-                MessageType::ServerNotice(message) => {
-                    let child = if let Some(Ok(child)) =
-                        parent.child().map(|w| w.downcast::<MessageText>())
-                    {
-                        child
-                    } else {
-                        let child = MessageText::new();
-                        parent.set_child(Some(&child));
-                        child
-                    };
-                    child.text(message.body);
-                }
-                MessageType::Text(message) => {
-                    let child = if let Some(Ok(child)) =
-                        parent.child().map(|w| w.downcast::<MessageText>())
-                    {
-                        child
-                    } else {
-                        let child = MessageText::new();
-                        parent.set_child(Some(&child));
-                        child
-                    };
-                    child.markup(message.formatted, message.body, &event.room());
-                }
-                MessageType::Video(message) => {
-                    let child = if let Some(Ok(child)) =
-                        parent.child().map(|w| w.downcast::<MessageMedia>())
-                    {
-                        child
-                    } else {
-                        let child = MessageMedia::new();
-                        parent.set_child(Some(&child));
-                        child
-                    };
-                    child.video(message, &event.room().session(), compact);
-                }
-                MessageType::VerificationRequest(_) => {
-                    // TODO: show more information about the verification
-                    let child = if let Some(Ok(child)) =
-                        parent.child().map(|w| w.downcast::<MessageText>())
-                    {
-                        child
-                    } else {
-                        let child = MessageText::new();
-                        parent.set_child(Some(&child));
-                        child
-                    };
-                    child.text(gettext("Identity verification was started"));
-                }
-                _ => {
-                    warn!("Event not supported: {:?}", msgtype);
-                    let child = if let Some(Ok(child)) =
-                        parent.child().map(|w| w.downcast::<MessageText>())
-                    {
-                        child
-                    } else {
-                        let child = MessageText::new();
-                        parent.set_child(Some(&child));
-                        child
-                    };
-                    child.text(gettext("Unsupported event"));
-                }
-            }
-        }
-        Some(AnyMessageLikeEventContent::Sticker(content)) => {
-            let child =
-                if let Some(Ok(child)) = parent.child().map(|w| w.downcast::<MessageMedia>()) {
-                    child
-                } else {
-                    let child = MessageMedia::new();
-                    parent.set_child(Some(&child));
-                    child
-                };
-            child.sticker(content, &event.room().session(), compact);
-        }
-        Some(AnyMessageLikeEventContent::RoomEncrypted(_)) => {
-            let child = if let Some(Ok(child)) = parent.child().map(|w| w.downcast::<MessageText>())
-            {
-                child
-            } else {
-                let child = MessageText::new();
-                parent.set_child(Some(&child));
-                child
-            };
-            child.text(gettext("Unable to decrypt this message, decryption will be retried once the keys are 
available."));
-        }
-        Some(AnyMessageLikeEventContent::RoomRedaction(_)) => {
-            let child = if let Some(Ok(child)) = parent.child().map(|w| w.downcast::<MessageText>())
-            {
-                child
-            } else {
-                let child = MessageText::new();
-                parent.set_child(Some(&child));
-                child
-            };
-            child.text(gettext("This message was removed."));
-        }
-        _ => {
-            let child = if let Some(Ok(child)) = parent.child().map(|w| w.downcast::<MessageText>())
-            {
-                child
-            } else {
-                let child = MessageText::new();
-                parent.set_child(Some(&child));
-                child
-            };
-            child.text(gettext("Unsupported event"));
-        }
-    }
-}
diff --git a/src/session/content/room_history/message_row/text.rs 
b/src/session/content/room_history/message_row/text.rs
index f799f814b..0a3446e7a 100644
--- a/src/session/content/room_history/message_row/text.rs
+++ b/src/session/content/room_history/message_row/text.rs
@@ -14,6 +14,7 @@ use matrix_sdk::ruma::{
 };
 use sourceview::prelude::*;
 
+use super::ContentFormat;
 use crate::{
     components::{LabelWithWidgets, Pill, DEFAULT_PLACEHOLDER},
     session::{room::Member, Room, UserExt},
@@ -60,22 +61,28 @@ impl MessageText {
     }
 
     /// Display the given plain text.
-    pub fn text(&self, body: String) {
-        self.build_text(body, WithMentions::No);
+    pub fn text(&self, body: String, format: ContentFormat) {
+        self.build_text(body, WithMentions::No, format);
     }
 
     /// Display the given text with markup.
     ///
     /// It will detect if it should display the body or the formatted body.
-    pub fn markup(&self, formatted: Option<FormattedBody>, body: String, room: &Room) {
+    pub fn markup(
+        &self,
+        formatted: Option<FormattedBody>,
+        body: String,
+        room: &Room,
+        format: ContentFormat,
+    ) {
         if let Some(html_blocks) = formatted
             .filter(is_valid_formatted_body)
             .and_then(|formatted| parse_formatted_body(strip_reply(&formatted.body)))
         {
-            self.build_html(html_blocks, room);
+            self.build_html(html_blocks, room, format);
         } else {
             let body = linkify(strip_reply(&body));
-            self.build_text(body, WithMentions::Yes(room));
+            self.build_text(body, WithMentions::Yes(room), format);
         }
     }
 
@@ -88,6 +95,7 @@ impl MessageText {
         body: String,
         sender: Member,
         room: &Room,
+        format: ContentFormat,
     ) {
         if let Some(body) = formatted
             .filter(is_valid_formatted_body)
@@ -99,16 +107,17 @@ impl MessageText {
             };
 
             let html = parse_formatted_body(&formatted.body).unwrap();
-            self.build_html(html, room);
+            self.build_html(html, room, format);
         } else {
             self.build_text(
                 format!("{} {}", sender.html_mention(), linkify(&body)),
                 WithMentions::Yes(room),
+                format,
             );
         }
     }
 
-    fn build_text(&self, text: String, with_mentions: WithMentions) {
+    fn build_text(&self, text: String, with_mentions: WithMentions, format: ContentFormat) {
         let child = if let Some(Ok(child)) = self.child().map(|w| w.downcast::<LabelWithWidgets>())
         {
             child
@@ -134,15 +143,23 @@ impl MessageText {
             child.set_widgets(Vec::<gtk::Widget>::new());
             child.set_label(Some(text));
         }
+
+        child.set_ellipsize(format == ContentFormat::Ellipsized);
     }
 
-    fn build_html(&self, blocks: Vec<HtmlBlock>, room: &Room) {
+    fn build_html(&self, blocks: Vec<HtmlBlock>, room: &Room, format: ContentFormat) {
         let child = gtk::Box::new(gtk::Orientation::Vertical, 6);
         self.set_child(Some(&child));
 
+        let ellipsize = format == ContentFormat::Ellipsized;
+        let len = blocks.len();
         for block in blocks {
-            let widget = create_widget_for_html_block(&block, room);
+            let widget = create_widget_for_html_block(&block, room, ellipsize, len > 1);
             child.append(&widget);
+
+            if ellipsize {
+                break;
+            }
         }
     }
 }
@@ -176,14 +193,23 @@ fn parse_formatted_body(formatted: &str) -> Option<Vec<HtmlBlock>> {
     markup_html(formatted).ok()
 }
 
-fn create_widget_for_html_block(block: &HtmlBlock, room: &Room) -> gtk::Widget {
+fn create_widget_for_html_block(
+    block: &HtmlBlock,
+    room: &Room,
+    ellipsize: bool,
+    has_more: bool,
+) -> gtk::Widget {
     match block {
         HtmlBlock::Heading(n, s) => {
             let (label, widgets) = extract_mentions(s, room);
-            let label = hoverify_links(&label);
+            let mut label = hoverify_links(&label);
+            if ellipsize && has_more && !label.ends_with('…') && !label.ends_with("...") {
+                label.push('…');
+            }
             let w = LabelWithWidgets::with_label_and_widgets(&label, widgets);
             w.set_use_markup(true);
             w.add_css_class(&format!("h{}", n));
+            w.set_ellipsize(ellipsize);
             w.upcast::<gtk::Widget>()
         }
         HtmlBlock::UList(elements) => {
@@ -196,12 +222,24 @@ fn create_widget_for_html_block(block: &HtmlBlock, room: &Room) -> gtk::Widget {
                 let bullet = gtk::Label::new(Some("•"));
                 bullet.set_valign(gtk::Align::Start);
                 let (label, widgets) = extract_mentions(li, room);
-                let label = hoverify_links(&label);
+                let mut label = hoverify_links(&label);
+                if ellipsize
+                    && (has_more || elements.len() > 1)
+                    && !label.ends_with('…')
+                    && !label.ends_with("...")
+                {
+                    label.push('…');
+                }
                 let w = LabelWithWidgets::with_label_and_widgets(&label, widgets);
                 w.set_use_markup(true);
+                w.set_ellipsize(ellipsize);
                 h_box.append(&bullet);
                 h_box.append(&w);
                 bx.append(&h_box);
+
+                if ellipsize {
+                    break;
+                }
             }
 
             bx.upcast::<gtk::Widget>()
@@ -216,43 +254,82 @@ fn create_widget_for_html_block(block: &HtmlBlock, room: &Room) -> gtk::Widget {
                 let bullet = gtk::Label::new(Some(&format!("{}.", i + 1)));
                 bullet.set_valign(gtk::Align::Start);
                 let (label, widgets) = extract_mentions(ol, room);
-                let label = hoverify_links(&label);
+                let mut label = hoverify_links(&label);
+                if ellipsize
+                    && (has_more || elements.len() > 1)
+                    && !label.ends_with('…')
+                    && !label.ends_with("...")
+                {
+                    label.push('…');
+                }
                 let w = LabelWithWidgets::with_label_and_widgets(&label, widgets);
                 w.set_use_markup(true);
+                w.set_ellipsize(ellipsize);
                 h_box.append(&bullet);
                 h_box.append(&w);
                 bx.append(&h_box);
+
+                if ellipsize {
+                    break;
+                }
             }
 
             bx.upcast::<gtk::Widget>()
         }
         HtmlBlock::Code(s) => {
-            let scrolled = gtk::ScrolledWindow::new();
-            scrolled.set_policy(gtk::PolicyType::Automatic, gtk::PolicyType::Never);
-            let buffer = sourceview::Buffer::new(None);
-            buffer.set_highlight_matching_brackets(false);
-            buffer.set_text(s);
-            crate::utils::setup_style_scheme(&buffer);
-            let view = sourceview::View::with_buffer(&buffer);
-            view.set_editable(false);
-            view.add_css_class("codeview");
-            scrolled.set_child(Some(&view));
-            scrolled.upcast::<gtk::Widget>()
+            if ellipsize {
+                let label = if let Some(pos) = s.find('\n') {
+                    format!("<tt>{}…</tt>", &s[0..pos])
+                } else if has_more {
+                    format!("<tt>{s}…</tt>")
+                } else {
+                    format!("<tt>{s}</tt>")
+                };
+                let w = LabelWithWidgets::with_label_and_widgets(&label, Vec::<gtk::Widget>::new());
+                w.set_use_markup(true);
+                w.set_ellipsize(ellipsize);
+                w.upcast::<gtk::Widget>()
+            } else {
+                let scrolled = gtk::ScrolledWindow::new();
+                scrolled.set_policy(gtk::PolicyType::Automatic, gtk::PolicyType::Never);
+                let buffer = sourceview::Buffer::new(None);
+                buffer.set_highlight_matching_brackets(false);
+                buffer.set_text(s);
+                crate::utils::setup_style_scheme(&buffer);
+                let view = sourceview::View::with_buffer(&buffer);
+                view.set_editable(false);
+                view.add_css_class("codeview");
+                scrolled.set_child(Some(&view));
+                scrolled.upcast::<gtk::Widget>()
+            }
         }
         HtmlBlock::Quote(blocks) => {
             let bx = gtk::Box::new(gtk::Orientation::Vertical, 6);
             bx.add_css_class("quote");
             for block in blocks.iter() {
-                let w = create_widget_for_html_block(block, room);
+                let w = create_widget_for_html_block(
+                    block,
+                    room,
+                    ellipsize,
+                    has_more || blocks.len() > 1,
+                );
                 bx.append(&w);
+
+                if ellipsize {
+                    break;
+                }
             }
             bx.upcast::<gtk::Widget>()
         }
         HtmlBlock::Text(s) => {
             let (label, widgets) = extract_mentions(s, room);
-            let label = hoverify_links(&label);
+            let mut label = hoverify_links(&label);
+            if ellipsize && has_more && !label.ends_with('…') && !label.ends_with("...") {
+                label.push('…');
+            }
             let w = LabelWithWidgets::with_label_and_widgets(&label, widgets);
             w.set_use_markup(true);
+            w.set_ellipsize(ellipsize);
             w.upcast::<gtk::Widget>()
         }
     }


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