[fractal/fractal-next] content: Show image messages in the timeline



commit 68c146d6fb2159c13fbac58642ee150339c3380c
Author: Kévin Commaille <zecakeh tedomum fr>
Date:   Thu Nov 25 19:48:44 2021 +0100

    content: Show image messages in the timeline

 data/resources/style.css                 |   4 +
 po/POTFILES.in                           |   1 +
 src/meson.build                          |   1 +
 src/session/content/message_row/image.rs | 267 +++++++++++++++++++++++++++++++
 src/session/content/message_row/mod.rs   |   8 +-
 src/session/content/room_history.rs      |   4 +
 6 files changed, 283 insertions(+), 2 deletions(-)
---
diff --git a/data/resources/style.css b/data/resources/style.css
index 45a58304..8475ff11 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -191,6 +191,10 @@ headerbar.flat {
   margin-left: 46px;
 }
 
+.room-history .event-content .thumbnail {
+  border-radius: 6px;
+}
+
 .divider-row {
   font-size: 0.9em;
   font-weight: bold;
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 1278d7ca..e4cc9f4f 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -77,6 +77,7 @@ src/session/content/item_row.rs
 src/session/content/invite.rs
 src/session/content/markdown_popover.rs
 src/session/content/message_row/file.rs
+src/session/content/message_row/image.rs
 src/session/content/message_row/mod.rs
 src/session/content/message_row/text.rs
 src/session/content/mod.rs
diff --git a/src/meson.build b/src/meson.build
index 0df36dcf..8dbaccbc 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -62,6 +62,7 @@ sources = files(
   'session/content/invite.rs',
   'session/content/markdown_popover.rs',
   'session/content/message_row/file.rs',
+  'session/content/message_row/image.rs',
   'session/content/message_row/mod.rs',
   'session/content/message_row/text.rs',
   'session/content/mod.rs',
diff --git a/src/session/content/message_row/image.rs b/src/session/content/message_row/image.rs
new file mode 100644
index 00000000..88069ab4
--- /dev/null
+++ b/src/session/content/message_row/image.rs
@@ -0,0 +1,267 @@
+use std::convert::TryInto;
+
+use adw::{prelude::BinExt, subclass::prelude::*};
+use gettextrs::gettext;
+use gtk::{
+    gdk,
+    gdk_pixbuf::Pixbuf,
+    gio,
+    glib::{self, clone},
+    prelude::*,
+    subclass::prelude::*,
+};
+use log::warn;
+use matrix_sdk::{
+    media::{MediaEventContent, MediaThumbnailSize},
+    ruma::{
+        api::client::r0::media::get_content_thumbnail::Method,
+        events::room::{message::ImageMessageEventContent, ImageInfo},
+        uint,
+    },
+};
+
+use crate::{session::Session, spawn, spawn_tokio};
+
+mod imp {
+    use std::cell::Cell;
+
+    use once_cell::sync::Lazy;
+
+    use super::*;
+
+    #[derive(Debug, Default)]
+    pub struct MessageImage {
+        /// The intended display width of the full image.
+        pub width: Cell<i32>,
+        /// The intended display height of the full image.
+        pub height: Cell<i32>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for MessageImage {
+        const NAME: &'static str = "ContentMessageImage";
+        type Type = super::MessageImage;
+        type ParentType = adw::Bin;
+    }
+
+    impl ObjectImpl for MessageImage {
+        fn properties() -> &'static [glib::ParamSpec] {
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![
+                    glib::ParamSpec::new_int(
+                        "width",
+                        "Width",
+                        "The intended display width of the full image",
+                        -1,
+                        i32::MAX,
+                        -1,
+                        glib::ParamFlags::WRITABLE,
+                    ),
+                    glib::ParamSpec::new_int(
+                        "height",
+                        "Height",
+                        "The intended display height of the full image",
+                        -1,
+                        i32::MAX,
+                        -1,
+                        glib::ParamFlags::WRITABLE,
+                    ),
+                ]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn set_property(
+            &self,
+            _obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.name() {
+                "width" => {
+                    self.width.set(value.get().unwrap());
+                }
+                "height" => {
+                    self.height.set(value.get().unwrap());
+                }
+                _ => unimplemented!(),
+            }
+        }
+
+        fn constructed(&self, obj: &Self::Type) {
+            self.parent_constructed(obj);
+
+            // We need to control the value returned by `measure`.
+            obj.set_layout_manager(gtk::NONE_LAYOUT_MANAGER);
+        }
+    }
+
+    impl WidgetImpl for MessageImage {
+        fn measure(
+            &self,
+            obj: &Self::Type,
+            orientation: gtk::Orientation,
+            for_size: i32,
+        ) -> (i32, i32, i32, i32) {
+            match obj.child() {
+                Some(child) => {
+                    // The GdkPaintable will keep its ratio, so we only need to control the height.
+                    if orientation == gtk::Orientation::Vertical {
+                        let original_width = self.width.get();
+                        let original_height = self.height.get();
+
+                        // We limit the thumbnail's width to 320 pixels.
+                        let width = for_size.min(320);
+
+                        let nat_height = if original_height > 0 && original_width > 0 {
+                            // We don't want the image to be upscaled.
+                            let width = width.min(original_width);
+                            width * original_height / original_width
+                        } else {
+                            // Get the natural height of the image data.
+                            child.measure(orientation, width).1
+                        };
+
+                        // We limit the thumbnail's height to 240 pixels.
+                        let height = nat_height.min(240);
+                        (0, height, -1, -1)
+                    } else {
+                        child.measure(orientation, for_size)
+                    }
+                }
+                None => (0, 0, -1, -1),
+            }
+        }
+
+        fn request_mode(&self, _obj: &Self::Type) -> gtk::SizeRequestMode {
+            gtk::SizeRequestMode::HeightForWidth
+        }
+
+        fn size_allocate(&self, obj: &Self::Type, _width: i32, height: i32, baseline: i32) {
+            if let Some(child) = obj.child() {
+                // We need to allocate just enough width to the child so it doesn't expand.
+                let original_width = self.width.get();
+                let original_height = self.height.get();
+                let width = if original_height > 0 && original_width > 0 {
+                    height * original_width / original_height
+                } else {
+                    // Get the natural width of the image data.
+                    child.measure(gtk::Orientation::Horizontal, height).1
+                };
+
+                child.allocate(width, height, baseline, None);
+            }
+        }
+    }
+
+    impl BinImpl for MessageImage {}
+}
+
+glib::wrapper! {
+    /// A widget displaying an image message.
+    pub struct MessageImage(ObjectSubclass<imp::MessageImage>)
+        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+impl MessageImage {
+    pub fn image(image: ImageMessageEventContent, session: &Session) -> Self {
+        let (width, height) = get_width_height(image.info.as_deref());
+
+        let self_: Self = glib::Object::new(&[("width", &width), ("height", &height)])
+            .expect("Failed to create MessageImage");
+        self_.build(image, session);
+        self_
+    }
+
+    fn build<C>(&self, content: C, session: &Session)
+    where
+        C: MediaEventContent + Send + Sync + 'static,
+    {
+        let client = session.client();
+        let handle = match content.thumbnail() {
+            Some(_) => {
+                spawn_tokio!(async move {
+                    client
+                        .get_thumbnail(
+                            content,
+                            MediaThumbnailSize {
+                                method: Method::Scale,
+                                width: uint!(320),
+                                height: uint!(240),
+                            },
+                            true,
+                        )
+                        .await
+                })
+            }
+            None => {
+                spawn_tokio!(async move { client.get_file(content, true,).await })
+            }
+        };
+
+        spawn!(
+            glib::PRIORITY_LOW,
+            clone!(@weak self as obj => async move {
+                match handle.await.unwrap() {
+                    Ok(Some(data)) => {
+                        let stream = gio::MemoryInputStream::from_bytes(&glib::Bytes::from(&data));
+                        let texture = Pixbuf::from_stream(&stream, gio::NONE_CANCELLABLE)
+                            .ok()
+                            .map(|pixbuf| gdk::Texture::for_pixbuf(&pixbuf));
+                        let child = gtk::Picture::for_paintable(texture.as_ref());
+
+                        // To get rounded corners
+                        child.set_overflow(gtk::Overflow::Hidden);
+                        child.add_css_class("thumbnail");
+
+                        obj.set_child(Some(&child));
+                        obj.queue_resize();
+                    }
+                    Ok(None) => {
+                        warn!("Could not retrieve invalid image file");
+                        let child = gtk::Label::new(Some(&gettext("Could not retrieve image")));
+                        obj.set_child(Some(&child));
+                    }
+                    Err(error) => {
+                        warn!("Could not retrieve image file: {}", error);
+                        let child = gtk::Label::new(Some(&gettext("Could not retrieve image")));
+                        obj.set_child(Some(&child));
+                    }
+                }
+            })
+        );
+    }
+}
+
+/// Gets the width and height of the full image in info.
+///
+/// Returns a (width, height) tuple with either value set to -1 if it wasn't found.
+fn get_width_height(info: Option<&ImageInfo>) -> (i32, i32) {
+    let width = info
+        .and_then(|info| info.width)
+        .and_then(|ui| {
+            let u: Option<u16> = ui.try_into().ok();
+            u
+        })
+        .and_then(|u| {
+            let i: i32 = u.into();
+            Some(i)
+        })
+        .unwrap_or(-1);
+
+    let height = info
+        .and_then(|info| info.height)
+        .and_then(|ui| {
+            let u: Option<u16> = ui.try_into().ok();
+            u
+        })
+        .and_then(|u| {
+            let i: i32 = u.into();
+            Some(i)
+        })
+        .unwrap_or(-1);
+
+    (width, height)
+}
diff --git a/src/session/content/message_row/mod.rs b/src/session/content/message_row/mod.rs
index 461e01c3..288197a9 100644
--- a/src/session/content/message_row/mod.rs
+++ b/src/session/content/message_row/mod.rs
@@ -1,4 +1,5 @@
 mod file;
+mod image;
 mod text;
 
 use crate::components::Avatar;
@@ -14,7 +15,7 @@ use matrix_sdk::ruma::events::{
     AnyMessageEventContent, AnySyncMessageEvent, AnySyncRoomEvent,
 };
 
-use self::{file::MessageFile, text::MessageText};
+use self::{file::MessageFile, image::MessageImage, text::MessageText};
 use crate::prelude::*;
 use crate::session::room::Event;
 
@@ -267,7 +268,10 @@ impl MessageRow {
                         let child = MessageFile::new(Some(filename));
                         priv_.content.set_child(Some(&child));
                     }
-                    MessageType::Image(_message) => {}
+                    MessageType::Image(message) => {
+                        let child = MessageImage::image(message, &event.room().session());
+                        priv_.content.set_child(Some(&child));
+                    }
                     MessageType::Location(_message) => {}
                     MessageType::Notice(message) => {
                         let child = MessageText::markup(message.formatted, message.body);
diff --git a/src/session/content/room_history.rs b/src/session/content/room_history.rs
index 1ef7785a..ac1166ac 100644
--- a/src/session/content/room_history.rs
+++ b/src/session/content/room_history.rs
@@ -177,6 +177,10 @@ mod imp {
         }
 
         fn constructed(&self, obj: &Self::Type) {
+            // Needed to use the natural height of GtkPictures
+            self.listview
+                .set_vscroll_policy(gtk::ScrollablePolicy::Natural);
+
             obj.set_sticky(true);
             let adj = self.listview.vadjustment().unwrap();
 


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