[fractal/fractal-next] content: Show image messages in the timeline
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal/fractal-next] content: Show image messages in the timeline
- Date: Thu, 25 Nov 2021 22:55:40 +0000 (UTC)
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]