[fractal/fractal-next] room-history: Show video messages in the timeline



commit 8193be8e2c8352a4c8e777880f7ee3f358a9b3cf
Author: Kévin Commaille <zecakeh tedomum fr>
Date:   Thu Dec 9 12:06:11 2021 +0100

    room-history: Show video messages in the timeline

 build-aux/org.gnome.FractalNext.Devel.json         |   1 +
 data/resources/resources.gresource.xml             |   1 +
 data/resources/style.css                           |   5 +
 data/resources/ui/components-video-player.ui       |  32 +++
 data/resources/ui/media-viewer.ui                  |   4 +-
 po/POTFILES.in                                     |   3 +
 src/components/mod.rs                              |   2 +
 src/components/video_player.rs                     |  84 ++++++++
 src/meson.build                                    |   2 +
 .../content/room_history/message_row/image.rs      |  48 +----
 .../content/room_history/message_row/mod.rs        |   8 +-
 .../content/room_history/message_row/video.rs      | 233 +++++++++++++++++++++
 src/session/media_viewer.rs                        |  63 +++++-
 src/session/room/event.rs                          |  12 +-
 src/utils.rs                                       |  17 ++
 15 files changed, 470 insertions(+), 45 deletions(-)
---
diff --git a/build-aux/org.gnome.FractalNext.Devel.json b/build-aux/org.gnome.FractalNext.Devel.json
index 6c3c8f98..5fbafef2 100644
--- a/build-aux/org.gnome.FractalNext.Devel.json
+++ b/build-aux/org.gnome.FractalNext.Devel.json
@@ -11,6 +11,7 @@
     "finish-args" : [
         "--socket=fallback-x11",
         "--socket=wayland",
+        "--socket=pulseaudio",
         "--share=network",
         "--share=ipc",
         "--device=dri",
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 8840f519..e9d7f1b8 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -54,6 +54,7 @@
     <file compressed="true" preprocess="xml-stripblanks" 
alias="verification-emoji.ui">ui/verification-emoji.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="incoming-verification.ui">ui/incoming-verification.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="qr-code-scanner.ui">ui/qr-code-scanner.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="components-video-player.ui">ui/components-video-player.ui</file>
     <file compressed="true">style.css</file>
     <file preprocess="xml-stripblanks">icons/scalable/actions/send-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/status/welcome.svg</file>
diff --git a/data/resources/style.css b/data/resources/style.css
index 23b12be7..6b4a32cb 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -214,6 +214,11 @@ headerbar.flat {
   border-radius: 6px;
 }
 
+.room-history .event-content .thumbnail .timestamp {
+  border-radius: 4px;
+  padding: 2px 5px;
+}
+
 .room-history .event-content .quote {
   border-left: 2px solid @theme_selected_bg_color;
   padding-left: 6px;
diff --git a/data/resources/ui/components-video-player.ui b/data/resources/ui/components-video-player.ui
new file mode 100644
index 00000000..85dfc25d
--- /dev/null
+++ b/data/resources/ui/components-video-player.ui
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="ComponentsVideoPlayer" parent="AdwBin">
+    <style>
+      <class name="thumbnail"/>
+    </style>
+    <property name="overflow">hidden</property>
+    <child>
+      <object class="GtkOverlay">
+        <child>
+          <object class="GtkPicture" id="video"/>
+        </child>
+        <child type="overlay">
+          <object class="GtkLabel" id="timestamp">
+            <style>
+              <class name="osd"/>
+              <class name="timestamp"/>
+            </style>
+            <property name="halign">1</property>
+            <property name="valign">1</property>
+            <property name="margin-start">5</property>
+            <property name="margin-top">5</property>
+            <property name="label">00:00</property>
+            <layout>
+              <property name="measure">1</property>
+            </layout>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/data/resources/ui/media-viewer.ui b/data/resources/ui/media-viewer.ui
index a3bccafb..3102ddc3 100644
--- a/data/resources/ui/media-viewer.ui
+++ b/data/resources/ui/media-viewer.ui
@@ -24,7 +24,7 @@
                 <child type="start">
                   <object class="GtkButton" id="back">
                     <property name="icon-name">go-previous-symbolic</property>
-                    <property name="action-name">session.show-content</property>
+                    <property name="action-name">media-viewer.close</property>
                   </object>
                 </child>
                 <child type="end">
@@ -44,6 +44,8 @@
             <child>
               <object class="AdwBin" id="media">
                 <property name="halign">center</property>
+                <property name="valign">center</property>
+                <property name="vexpand">true</property>
               </object>
             </child>
           </object>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index ff8fc197..9bba3ee6 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -12,6 +12,7 @@ data/resources/ui/account-settings-devices-page.ui
 data/resources/ui/components-auth-dialog.ui
 data/resources/ui/components-avatar.ui
 data/resources/ui/components-loading-listbox-row.ui
+data/resources/ui/components-video-player.ui
 data/resources/ui/avatar-with-selection.ui
 data/resources/ui/content-divider-row.ui
 data/resources/ui/content-item.ui
@@ -58,6 +59,7 @@ src/components/in_app_notification.rs
 src/components/mod.rs
 src/components/spinner_button.rs
 src/components/pill.rs
+src/components/video_player.rs
 src/contrib/mod.rs
 src/contrib/qr_code.rs
 src/error.rs
@@ -89,6 +91,7 @@ src/session/content/room_history/message_row/file.rs
 src/session/content/room_history/message_row/image.rs
 src/session/content/room_history/message_row/mod.rs
 src/session/content/room_history/message_row/text.rs
+src/session/content/room_history/message_row/video.rs
 src/session/content/room_history/mod.rs
 src/session/content/room_history/state_row.rs
 src/session/content/room_history/state_row/mod.rs
diff --git a/src/components/mod.rs b/src/components/mod.rs
index 5ea7aa17..5c005a97 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -9,6 +9,7 @@ mod loading_listbox_row;
 mod pill;
 mod room_title;
 mod spinner_button;
+mod video_player;
 
 pub use self::auth_dialog::{AuthData, AuthDialog};
 pub use self::avatar::Avatar;
@@ -21,3 +22,4 @@ pub use self::loading_listbox_row::LoadingListBoxRow;
 pub use self::pill::Pill;
 pub use self::room_title::RoomTitle;
 pub use self::spinner_button::SpinnerButton;
+pub use self::video_player::VideoPlayer;
diff --git a/src/components/video_player.rs b/src/components/video_player.rs
new file mode 100644
index 00000000..7cd04b67
--- /dev/null
+++ b/src/components/video_player.rs
@@ -0,0 +1,84 @@
+use adw::subclass::prelude::*;
+use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
+
+mod imp {
+    use super::*;
+    use glib::subclass::InitializingObject;
+    use std::cell::RefCell;
+
+    #[derive(Debug, Default, CompositeTemplate)]
+    #[template(resource = "/org/gnome/FractalNext/components-video-player.ui")]
+    pub struct VideoPlayer {
+        pub media_file: RefCell<Option<gtk::MediaFile>>,
+        #[template_child]
+        pub video: TemplateChild<gtk::Picture>,
+        #[template_child]
+        pub timestamp: TemplateChild<gtk::Label>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for VideoPlayer {
+        const NAME: &'static str = "ComponentsVideoPlayer";
+        type Type = super::VideoPlayer;
+        type ParentType = adw::Bin;
+
+        fn class_init(klass: &mut Self::Class) {
+            Self::bind_template(klass);
+        }
+
+        fn instance_init(obj: &InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for VideoPlayer {}
+
+    impl WidgetImpl for VideoPlayer {}
+
+    impl BinImpl for VideoPlayer {}
+}
+
+glib::wrapper! {
+    /// A widget displaying a video media file.
+    pub struct VideoPlayer(ObjectSubclass<imp::VideoPlayer>)
+        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+impl VideoPlayer {
+    pub fn new(media_file: &gtk::MediaFile) -> Self {
+        let self_: Self = glib::Object::new(&[]).expect("Failed to create VideoPlayer");
+        self_.build(media_file);
+        self_
+    }
+
+    pub fn build(&self, media_file: &gtk::MediaFile) {
+        let priv_ = imp::VideoPlayer::from_instance(self);
+
+        priv_.video.set_paintable(Some(media_file));
+        let timestamp = &*priv_.timestamp;
+        media_file.connect_duration_notify(clone!(@weak timestamp => move |media_file| {
+            timestamp.set_label(&duration(media_file));
+        }));
+    }
+}
+
+/// Get the duration of `media_file` as a `String`.
+fn duration(media_file: &gtk::MediaFile) -> String {
+    let mut time = media_file.duration() / 1000000;
+
+    let sec = time % 60;
+    time = time - sec;
+    let min = (time % (60 * 60)) / 60;
+    time = time - (min * 60);
+    let hour = time / (60 * 60);
+
+    if hour > 0 {
+        // FIXME: Find how to localize this.
+        // hour:minutes:seconds
+        format!("{}:{:02}:{:02}", hour, min, sec)
+    } else {
+        // FIXME: Find how to localize this.
+        // minutes:seconds
+        format!("{:02}:{:02}", min, sec)
+    }
+}
diff --git a/src/meson.build b/src/meson.build
index 5b1a394d..a4e93ffb 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -37,6 +37,7 @@ sources = files(
   'components/in_app_notification.rs',
   'components/spinner_button.rs',
   'components/loading_listbox_row.rs',
+  'components/video_player.rs',
   'config.rs',
   'error.rs',
   'main.rs',
@@ -66,6 +67,7 @@ sources = files(
   'session/content/room_history/message_row/image.rs',
   'session/content/room_history/message_row/mod.rs',
   'session/content/room_history/message_row/text.rs',
+  'session/content/room_history/message_row/video.rs',
   'session/content/room_history/mod.rs',
   'session/content/room_history/state_row/creation.rs',
   'session/content/room_history/state_row/mod.rs',
diff --git a/src/session/content/room_history/message_row/image.rs 
b/src/session/content/room_history/message_row/image.rs
index 29f5496e..8a46e784 100644
--- a/src/session/content/room_history/message_row/image.rs
+++ b/src/session/content/room_history/message_row/image.rs
@@ -1,5 +1,3 @@
-use std::convert::TryInto;
-
 use adw::{prelude::BinExt, subclass::prelude::*};
 use gettextrs::gettext;
 use gtk::{
@@ -15,15 +13,12 @@ use matrix_sdk::{
     media::{MediaEventContent, MediaThumbnailSize},
     ruma::{
         api::client::r0::media::get_content_thumbnail::Method,
-        events::{
-            room::{message::ImageMessageEventContent, ImageInfo},
-            sticker::StickerEventContent,
-        },
+        events::{room::message::ImageMessageEventContent, sticker::StickerEventContent},
         uint,
     },
 };
 
-use crate::{session::Session, spawn, spawn_tokio};
+use crate::{session::Session, spawn, spawn_tokio, utils::uint_to_i32};
 
 #[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::GEnum)]
 #[repr(u32)]
@@ -222,7 +217,9 @@ glib::wrapper! {
 
 impl MessageImage {
     pub fn image(image: ImageMessageEventContent, session: &Session) -> Self {
-        let (width, height) = get_width_height(image.info.as_deref());
+        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 self_: Self = glib::Object::new(&[("width", &width), ("height", &height)])
             .expect("Failed to create MessageImage");
@@ -231,7 +228,9 @@ impl MessageImage {
     }
 
     pub fn sticker(sticker: StickerEventContent, session: &Session) -> Self {
-        let (width, height) = get_width_height(Some(&sticker.info));
+        let info = &sticker.info;
+        let width = uint_to_i32(info.width);
+        let height = uint_to_i32(info.height);
 
         let self_: Self = glib::Object::new(&[
             ("media-type", &MediaType::Sticker),
@@ -319,34 +318,3 @@ impl MessageImage {
         );
     }
 }
-
-/// 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/room_history/message_row/mod.rs 
b/src/session/content/room_history/message_row/mod.rs
index 9e88e210..d39e9b63 100644
--- a/src/session/content/room_history/message_row/mod.rs
+++ b/src/session/content/room_history/message_row/mod.rs
@@ -1,6 +1,7 @@
 mod file;
 mod image;
 mod text;
+mod video;
 
 use crate::components::Avatar;
 use adw::{prelude::*, subclass::prelude::*};
@@ -15,7 +16,7 @@ use matrix_sdk::ruma::events::{
     AnyMessageEventContent, AnySyncMessageEvent, AnySyncRoomEvent,
 };
 
-use self::{file::MessageFile, image::MessageImage, text::MessageText};
+use self::{file::MessageFile, image::MessageImage, text::MessageText, video::MessageVideo};
 use crate::prelude::*;
 use crate::session::room::Event;
 
@@ -285,7 +286,10 @@ impl MessageRow {
                         let child = MessageText::markup(message.formatted, message.body);
                         priv_.content.set_child(Some(&child));
                     }
-                    MessageType::Video(_message) => {}
+                    MessageType::Video(message) => {
+                        let child = MessageVideo::new(message, &event.room().session());
+                        priv_.content.set_child(Some(&child));
+                    }
                     MessageType::VerificationRequest(_message) => {}
                     _ => {
                         warn!("Event not supported: {:?}", msgtype)
diff --git a/src/session/content/room_history/message_row/video.rs 
b/src/session/content/room_history/message_row/video.rs
new file mode 100644
index 00000000..d88c1058
--- /dev/null
+++ b/src/session/content/room_history/message_row/video.rs
@@ -0,0 +1,233 @@
+use adw::{prelude::BinExt, subclass::prelude::*};
+use gettextrs::gettext;
+use gtk::{
+    gio,
+    glib::{self, clone},
+    prelude::*,
+    subclass::prelude::*,
+};
+use log::warn;
+use matrix_sdk::ruma::events::room::message::VideoMessageEventContent;
+
+use crate::{
+    components::VideoPlayer,
+    session::Session,
+    spawn, spawn_tokio,
+    utils::{cache_dir, uint_to_i32},
+};
+
+mod imp {
+    use std::cell::Cell;
+
+    use once_cell::sync::Lazy;
+
+    use super::*;
+
+    #[derive(Debug, Default)]
+    pub struct MessageVideo {
+        /// The intended display width of the video.
+        pub width: Cell<i32>,
+        /// The intended display height of the video.
+        pub height: Cell<i32>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for MessageVideo {
+        const NAME: &'static str = "ContentMessageVideo";
+        type Type = super::MessageVideo;
+        type ParentType = adw::Bin;
+    }
+
+    impl ObjectImpl for MessageVideo {
+        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 video",
+                        -1,
+                        i32::MAX,
+                        -1,
+                        glib::ParamFlags::WRITABLE,
+                    ),
+                    glib::ParamSpec::new_int(
+                        "height",
+                        "Height",
+                        "The intended display height of the video",
+                        -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 MessageVideo {
+        fn measure(
+            &self,
+            obj: &Self::Type,
+            orientation: gtk::Orientation,
+            for_size: i32,
+        ) -> (i32, i32, i32, i32) {
+            match obj.child() {
+                Some(child) => {
+                    let original_width = self.width.get();
+                    let original_height = self.height.get();
+
+                    if orientation == gtk::Orientation::Vertical {
+                        // We limit the 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 paintable to be upscaled.
+                            let width = width.min(original_width);
+                            width * original_height / original_width
+                        } else {
+                            // Get the natural height of the data.
+                            child.measure(orientation, width).1
+                        };
+
+                        // We limit the height to 240 pixels.
+                        let height = nat_height.min(240);
+                        (0, height, -1, -1)
+                    } else {
+                        // We limit the height to 240 pixels.
+                        let height = for_size.min(240);
+
+                        let nat_width = if original_height > 0 && original_width > 0 {
+                            // We don't want the paintable to be upscaled.
+                            let height = height.min(original_height);
+                            height * original_width / original_height
+                        } else {
+                            // Get the natural height of the data.
+                            child.measure(orientation, height).1
+                        };
+
+                        // We limit the width to 320 pixels.
+                        let width = nat_width.min(320);
+                        (0, width, -1, -1)
+                    }
+                }
+                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 video data.
+                    child.measure(gtk::Orientation::Horizontal, height).1
+                };
+
+                child.allocate(width, height, baseline, None);
+            }
+        }
+    }
+
+    impl BinImpl for MessageVideo {}
+}
+
+glib::wrapper! {
+    /// A widget displaying an message's thumbnail.
+    pub struct MessageVideo(ObjectSubclass<imp::MessageVideo>)
+        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+impl MessageVideo {
+    pub fn new(video: VideoMessageEventContent, session: &Session) -> Self {
+        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 self_: Self = glib::Object::new(&[("width", &width), ("height", &height)])
+            .expect("Failed to create MessageVideo");
+        self_.build(video, session);
+        self_
+    }
+
+    fn build(&self, video: VideoMessageEventContent, session: &Session) {
+        let body = video.body.clone();
+        let client = session.client();
+        let handle = spawn_tokio!(async move { client.get_file(video, true,).await });
+
+        spawn!(
+            glib::PRIORITY_LOW,
+            clone!(@weak self as obj => async move {
+                match handle.await.unwrap() {
+                    Ok(Some(data)) => {
+                        // The GStreamer backend of GtkVideo doesn't work with input streams so
+                        // we need to store the file.
+                        // See: https://gitlab.gnome.org/GNOME/gtk/-/issues/4062
+                        let mut path = cache_dir();
+                        path.push(body);
+                        let file = gio::File::for_path(path);
+                        file.replace_contents(
+                            &data,
+                            None,
+                            false,
+                            gio::FileCreateFlags::REPLACE_DESTINATION,
+                            gio::NONE_CANCELLABLE,
+                        )
+                        .unwrap();
+                        let media_file = gtk::MediaFile::for_file(&file);
+                        media_file.set_muted(true);
+                        media_file.connect_prepared_notify(|media_file| media_file.play());
+
+                        let video_player = VideoPlayer::new(&media_file);
+
+                        obj.set_child(Some(&video_player));
+                    }
+                    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));
+                    }
+                }
+            })
+        );
+    }
+}
diff --git a/src/session/media_viewer.rs b/src/session/media_viewer.rs
index 28eda93e..9dd65536 100644
--- a/src/session/media_viewer.rs
+++ b/src/session/media_viewer.rs
@@ -9,7 +9,9 @@ use matrix_sdk::ruma::events::{room::message::MessageType, AnyMessageEventConten
 use crate::{
     components::{ContextMenuBin, ContextMenuBinImpl},
     session::room::Event,
-    spawn, Window,
+    spawn,
+    utils::cache_dir,
+    Window,
 };
 
 use super::room::EventActions;
@@ -45,6 +47,28 @@ mod imp {
 
         fn class_init(klass: &mut Self::Class) {
             Self::bind_template(klass);
+
+            klass.install_action("media-viewer.close", None, move |obj, _, _| {
+                let priv_ = imp::MediaViewer::from_instance(obj);
+                if let Some(stream) = priv_
+                    .media
+                    .child()
+                    .and_then(|w| w.downcast::<gtk::Video>().ok())
+                    .and_then(|video| video.media_stream())
+                {
+                    if stream.is_playing() {
+                        stream.pause();
+                        stream.seek(0);
+                    }
+                }
+                obj.activate_action("session.show-content", None);
+            });
+            klass.add_binding_action(
+                gdk::keys::constants::Escape,
+                gdk::ModifierType::empty(),
+                "media-viewer.close",
+                None,
+            );
         }
 
         fn instance_init(obj: &InitializingObject<Self>) {
@@ -264,6 +288,43 @@ impl MediaViewer {
                             })
                         );
                     }
+                    MessageType::Video(video) => {
+                        self.set_body(Some(video.body.clone()));
+
+                        spawn!(
+                            glib::PRIORITY_LOW,
+                            clone!(@weak self as obj => async move {
+                                let priv_ = imp::MediaViewer::from_instance(&obj);
+
+                                match event.get_media_content().await {
+                                    Ok((_, data)) => {
+                                        // The GStreamer backend of GtkVideo doesn't work with input streams 
so
+                                        // we need to store the file.
+                                        // See: https://gitlab.gnome.org/GNOME/gtk/-/issues/4062
+                                        let mut path = cache_dir();
+                                        path.push(video.body);
+                                        let file = gio::File::for_path(path);
+                                        file.replace_contents(
+                                            &data,
+                                            None,
+                                            false,
+                                            gio::FileCreateFlags::REPLACE_DESTINATION,
+                                            gio::NONE_CANCELLABLE,
+                                        )
+                                        .unwrap();
+                                        let child = gtk::Video::builder().file(&file).autoplay(true).build();
+
+                                        priv_.media.set_child(Some(&child));
+                                    }
+                                    Err(error) => {
+                                        warn!("Could not retrieve video file: {}", error);
+                                        let child = gtk::Label::new(Some(&gettext("Could not retrieve 
video")));
+                                        priv_.media.set_child(Some(&child));
+                                    }
+                                }
+                            })
+                        );
+                    }
                     _ => {}
                 }
             }
diff --git a/src/session/room/event.rs b/src/session/room/event.rs
index 9c63fb78..64141075 100644
--- a/src/session/room/event.rs
+++ b/src/session/room/event.rs
@@ -516,6 +516,7 @@ impl Event {
     ///
     /// - File message (`MessageType::File`).
     /// - Image message (`MessageType::Image`).
+    /// - Video message (`MessageType::Video`).
     ///
     /// Returns `Ok((filename, binary_content))` on success, `Err` if an error occured while
     /// fetching the content. Panics on an incompatible event.
@@ -535,6 +536,12 @@ impl Event {
                     let data = handle.await.unwrap()?.unwrap();
                     return Ok((filename, data));
                 }
+                MessageType::Video(content) => {
+                    let filename = content.body.clone();
+                    let handle = spawn_tokio!(async move { client.get_file(content, true).await });
+                    let data = handle.await.unwrap()?.unwrap();
+                    return Ok((filename, data));
+                }
                 _ => {}
             };
         };
@@ -546,7 +553,10 @@ impl Event {
     pub fn can_view_media(&self) -> bool {
         match self.message_content() {
             Some(AnyMessageEventContent::RoomMessage(message)) => {
-                matches!(message.msgtype, MessageType::Image(_))
+                matches!(
+                    message.msgtype,
+                    MessageType::Image(_) | MessageType::Video(_)
+                )
             }
             _ => false,
         }
diff --git a/src/utils.rs b/src/utils.rs
index e4b336a2..447a0d25 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -58,10 +58,12 @@ macro_rules! spawn_tokio {
     };
 }
 
+use std::convert::TryInto;
 use std::path::PathBuf;
 
 use gtk::gio::{self, prelude::*};
 use gtk::glib::{self, Object};
+use matrix_sdk::ruma::UInt;
 
 /// Returns an expression looking up the given property on `object`.
 pub fn prop_expr<T: IsA<Object>>(object: &T, prop: &str) -> gtk::Expression {
@@ -121,3 +123,18 @@ pub fn cache_dir() -> PathBuf {
 
     path
 }
+
+/// Converts a `UInt` to `i32`.
+///
+/// Returns `-1` if the conversion didn't work.
+pub fn uint_to_i32(u: Option<UInt>) -> i32 {
+    u.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)
+}


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