content: Show image messages in the timeline

This commit is contained in:
Kévin Commaille 2021-11-25 19:48:44 +01:00
parent fe8ad34895
commit 68c146d6fb
No known key found for this signature in database
GPG key ID: DD507DAE96E8245C
6 changed files with 283 additions and 2 deletions

View file

@ -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;

View file

@ -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

View file

@ -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',

View file

@ -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)
}

View file

@ -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);

View file

@ -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();