[fractal] attachment-dialog: Use MediaContentViewer



commit 74b5b025e2e6fbdc0c140f7fb0d97883372c43a5
Author: Kévin Commaille <zecakeh tedomum fr>
Date:   Sun Apr 24 16:07:09 2022 +0200

    attachment-dialog: Use MediaContentViewer
    
    Preview more file types
    
    Part-of: <https://gitlab.gnome.org/GNOME/fractal/-/merge_requests/1085>

 data/resources/ui/attachment-dialog.ui             |  19 +-
 .../content/room_history/attachment_dialog.rs      |  29 +--
 src/session/content/room_history/mod.rs            | 280 ++++++++++-----------
 src/session/room/mod.rs                            |  10 +-
 4 files changed, 158 insertions(+), 180 deletions(-)
---
diff --git a/data/resources/ui/attachment-dialog.ui b/data/resources/ui/attachment-dialog.ui
index 674aea5d5..8fce210a0 100644
--- a/data/resources/ui/attachment-dialog.ui
+++ b/data/resources/ui/attachment-dialog.ui
@@ -29,24 +29,7 @@
       </object>
     </property>
     <property name="child">
-      <object class="GtkStack" id="stack">
-        <child>
-          <object class="AdwStatusPage">
-            <property name="icon-name">face-sick-symbolic</property>
-            <property name="title" translatable="yes">No Preview Available</property>
-            <style>
-              <class name="compact"/>
-            </style>
-          </object>
-        </child>
-        <child>
-          <object class="GtkStackPage">
-            <property name="name">preview</property>
-            <property name="child">
-              <object class="GtkImage" id="preview"/>
-            </property>
-          </object>
-        </child>
+      <object class="ComponentsMediaContentViewer" id="media">
       </object>
     </property>
     <child>
diff --git a/src/session/content/room_history/attachment_dialog.rs 
b/src/session/content/room_history/attachment_dialog.rs
index 4e39c4bd2..6168aeee9 100644
--- a/src/session/content/room_history/attachment_dialog.rs
+++ b/src/session/content/room_history/attachment_dialog.rs
@@ -1,20 +1,16 @@
 use gtk::{gdk, gio, glib, prelude::*, subclass::prelude::*, CompositeTemplate};
 use once_cell::sync::Lazy;
 
-mod imp {
-    use std::cell::RefCell;
+use crate::components::MediaContentViewer;
 
+mod imp {
     use super::*;
 
     #[derive(Debug, Default, CompositeTemplate)]
     #[template(resource = "/org/gnome/Fractal/attachment-dialog.ui")]
     pub struct AttachmentDialog {
-        pub file: RefCell<Option<gio::File>>,
-        pub texture: RefCell<Option<gdk::Texture>>,
         #[template_child]
-        pub preview: TemplateChild<gtk::Image>,
-        #[template_child]
-        pub stack: TemplateChild<gtk::Stack>,
+        pub media: TemplateChild<MediaContentViewer>,
     }
 
     #[glib::object_subclass]
@@ -59,16 +55,17 @@ glib::wrapper! {
 }
 
 impl AttachmentDialog {
-    pub fn new(window: &gtk::Window) -> Self {
-        glib::Object::new(&[("transient-for", window)]).unwrap()
+    pub fn for_image(transient_for: &gtk::Window, title: &str, image: &gdk::Texture) -> Self {
+        let obj: Self = glib::Object::new(&[("transient-for", transient_for), ("title", &title)])
+            .expect("Failed to create AttachmentDialog");
+        obj.imp().media.view_image(image);
+        obj
     }
 
-    pub fn set_texture(&self, texture: &gdk::Texture) {
-        let priv_ = self.imp();
-        priv_.stack.set_visible_child_name("preview");
-
-        priv_
-            .preview
-            .set_paintable(Some(texture.upcast_ref::<gdk::Paintable>()));
+    pub fn for_file(transient_for: &gtk::Window, title: &str, file: &gio::File) -> Self {
+        let obj: Self = glib::Object::new(&[("transient-for", transient_for), ("title", &title)])
+            .expect("Failed to create AttachmentDialog");
+        obj.imp().media.view_file(file.to_owned());
+        obj
     }
 }
diff --git a/src/session/content/room_history/mod.rs b/src/session/content/room_history/mod.rs
index c838267ec..949189a16 100644
--- a/src/session/content/room_history/mod.rs
+++ b/src/session/content/room_history/mod.rs
@@ -21,6 +21,7 @@ use gtk::{
     subclass::prelude::*,
     CompositeTemplate,
 };
+use log::warn;
 use matrix_sdk::ruma::events::room::message::{
     EmoteMessageEventContent, FormattedBody, MessageType, RoomMessageEventContent,
     TextMessageEventContent,
@@ -33,7 +34,7 @@ use self::{
     state_row::StateRow, verification_info_bar::VerificationInfoBar,
 };
 use crate::{
-    components::{CustomEntry, DragOverlay, Pill, ReactionChooser, RoomTitle},
+    components::{CustomEntry, DragOverlay, Pill, ReactionChooser, RoomTitle, Toast},
     i18n::gettext_f,
     session::{
         content::{MarkdownPopover, RoomDetails},
@@ -41,16 +42,10 @@ use crate::{
         user::UserExt,
     },
     spawn,
+    utils::filename_for_mime,
+    window::Window,
 };
 
-const MIME_TYPES: &[&str] = &[
-    "image/png",
-    "image/jpeg",
-    "image/tiff",
-    "image/svg+xml",
-    "image/bmp",
-];
-
 mod imp {
     use std::cell::{Cell, RefCell};
 
@@ -328,19 +323,15 @@ mod imp {
             self.message_entry.add_controller(&key_events);
             self.message_entry
                 .connect_paste_clipboard(clone!(@weak obj => move |entry| {
-                    spawn!(
-                        glib::PRIORITY_DEFAULT_IDLE,
-                        clone!(@weak obj => async move {
-                            obj.read_clipboard().await;
-                    }));
-                    let clip = obj.clipboard();
+                    let formats = obj.clipboard().formats();
 
-                    // TODO Check if this is the most general condition on which
-                    // the clipboard contains more than text.
-                    let formats = clip.formats();
-                    let contains_mime = MIME_TYPES.iter().any(|mime| formats.contain_mime_type(mime));
-                    if formats.contains_type(gio::File::static_type()) || contains_mime {
+                    // We only handle files and supported images.
+                    if formats.contains_type(gio::File::static_type()) || 
formats.contains_type(gdk::Texture::static_type()) {
                         entry.stop_signal_emission_by_name("paste-clipboard");
+                        spawn!(
+                            clone!(@weak obj => async move {
+                                obj.read_clipboard().await;
+                        }));
                     }
                 }));
 
@@ -398,45 +389,53 @@ glib::wrapper! {
 impl RoomHistory {
     async fn read_clipboard(&self) {
         let clipboard = self.clipboard();
-
-        // Check if there is a png/jpg in the clipboard.
-        let res = clipboard
-            .read_future(MIME_TYPES, glib::PRIORITY_DEFAULT)
-            .await;
-        let body = match clipboard.read_text_future().await {
-            Ok(Some(body)) => std::path::Path::new(&body)
-                .file_name()
-                .unwrap()
-                .to_str()
-                .unwrap()
-                .to_string(),
-            _ => gettext("Image"),
-        };
-        if let Ok((stream, mime)) = res {
-            log::debug!("Found a {} in the clipboard", &mime);
-            if let Ok(bytes) = read_stream(&stream).await {
-                let mime = mime::Mime::from_str(&mime).unwrap();
-                self.open_attach_dialog(bytes, mime, &body);
-
-                return;
+        let formats = clipboard.formats();
+
+        if formats.contains_type(gdk::Texture::static_type()) {
+            // There is an image in the clipboard.
+            match clipboard
+                .read_value_future(gdk::Texture::static_type(), glib::PRIORITY_DEFAULT)
+                .await
+            {
+                Ok(value) => match value.get::<gdk::Texture>() {
+                    Ok(texture) => {
+                        self.send_image(texture).await;
+                        return;
+                    }
+                    Err(error) => warn!("Could not get GdkTexture from value: {error:?}"),
+                },
+                Err(error) => warn!("Could not get GdkTexture from the clipboard: {error:?}"),
             }
-        }
 
-        // Check if there is a file in the clipboard.
-        let res = clipboard
-            .read_value_future(gio::File::static_type(), glib::PRIORITY_DEFAULT)
-            .await;
-        if let Ok(value) = res {
-            if let Ok(file) = value.get::<gio::File>() {
-                log::debug!("Found a file in the clipboard");
-
-                // Under some circumstances, the file will be
-                // under a path we don't have access to.
-                if !file.query_exists(gio::Cancellable::NONE) {
-                    return;
-                }
+            if let Some(window) = self
+                .root()
+                .as_ref()
+                .and_then(|root| root.downcast_ref::<Window>())
+            {
+                window.add_toast(&Toast::new(&gettext("Error getting image from clipboard")));
+            }
+        } else if formats.contains_type(gio::File::static_type()) {
+            // There is a file in the clipboard.
+            match clipboard
+                .read_value_future(gio::File::static_type(), glib::PRIORITY_DEFAULT)
+                .await
+            {
+                Ok(value) => match value.get::<gio::File>() {
+                    Ok(file) => {
+                        self.send_file(file).await;
+                        return;
+                    }
+                    Err(error) => warn!("Could not get file from value: {error:?}"),
+                },
+                Err(error) => warn!("Could not get file from the clipboard: {error:?}"),
+            }
 
-                self.read_file(&file).await;
+            if let Some(window) = self
+                .root()
+                .as_ref()
+                .and_then(|root| root.downcast_ref::<Window>())
+            {
+                window.add_toast(&Toast::new(&gettext("Error getting file from clipboard")));
             }
         }
     }
@@ -774,47 +773,30 @@ impl RoomHistory {
         );
 
         target.connect_drop(
-            glib::clone!(@weak self as obj => @default-return false, move |target, value, _, _| {
-                let drop = target.current_drop().unwrap();
-
-                // We first try to read if we get a serialized image. In general
-                // we get files, but this is useful when reading a drag-n-drop
-                // from another sandboxed app.
-                let formats = drop.formats();
-                for mime in MIME_TYPES {
-                    if formats.contain_mime_type(mime) {
-                        log::debug!("Received drag & drop with mime type: {}", mime);
-                        drop.read_async(&[mime], glib::PRIORITY_DEFAULT, gio::Cancellable::NONE, 
glib::clone!(@weak obj => move |res| {
-                            if let Ok((stream, mime)) = res {
-                                crate::spawn!(glib::clone!(@weak obj => async move {
-                                    if let Ok(bytes) = read_stream(&stream).await {
-                                        // TODO Get the actual name of the file by reading
-                                        // the text/plain mime type.
-                                        let body = gettext("Image");
-                                        let mime = mime::Mime::from_str(&mime).unwrap();
-                                        obj.open_attach_dialog(bytes, mime, &body);
-                                    }
-                                }));
-                            }
+            clone!(@weak self as obj => @default-return false, move |_, value, _, _| {
+                match value.get::<gio::File>() {
+                    Ok(file) => {
+                        spawn!(clone!(@weak obj => async move {
+                            obj.send_file(file).await;
                         }));
-
-                        return true;
+                        true
                     }
-                }
+                    Err(error) => {
+                        warn!("Could not get file from drop: {error:?}");
+
+                        if let Some(window) = obj
+                            .root()
+                            .as_ref()
+                            .and_then(|root| root.downcast_ref::<Window>())
+                        {
+                            window.add_toast(
+                                &Toast::new(&gettext("Error getting file from drop"))
+                            );
+                        }
 
-                if let Ok(file) = value.get::<gio::File>() {
-                    if !file.query_exists(gio::Cancellable::NONE) {
-                        log::debug!("Received drag & drop file, but don't have permissions: {:?}", 
file.path());
-                        return false;
+                        false
                     }
-                    log::debug!("Received drag & drop file: {:?}", file.path());
-                    crate::spawn!(glib::clone!(@weak obj, @strong file => async move {
-                        obj.read_file(&file).await;
-                    }));
-
-                    return true;
                 }
-                false
             }),
         );
 
@@ -866,22 +848,21 @@ impl RoomHistory {
         Ok(())
     }
 
-    fn open_attach_dialog(&self, bytes: Vec<u8>, mime: mime::Mime, title: &str) {
+    async fn send_image(&self, image: gdk::Texture) {
         let window = self.root().unwrap().downcast::<gtk::Window>().unwrap();
-        let dialog = AttachmentDialog::new(&window);
-        let gbytes = glib::Bytes::from_owned(bytes);
-        if let Ok(texture) = gdk::Texture::from_bytes(&gbytes) {
-            dialog.set_texture(&texture);
-        }
+        let filename = filename_for_mime(Some(mime::IMAGE_PNG.as_ref()), None);
+        let dialog = AttachmentDialog::for_image(&window, &filename, &image);
 
-        dialog.set_title(Some(title));
-        let title = title.to_string();
         dialog.connect_local(
             "send",
             false,
-            glib::clone!(@weak self as obj => @default-return None, move |_| {
+            clone!(@weak self as obj, @strong image => @default-return None, move |_| {
                 if let Some(room) = obj.room() {
-                    room.send_attachment(&gbytes, mime.clone(), &title);
+                    room.send_attachment(
+                        image.save_to_png_bytes().to_vec(),
+                        mime::IMAGE_PNG,
+                        &filename,
+                    );
                 }
 
                 None
@@ -908,7 +889,7 @@ impl RoomHistory {
                     let file = dialog.file().unwrap();
 
                     crate::spawn!(glib::clone!(@weak obj, @strong file => async move {
-                        obj.read_file(&file).await;
+                        obj.send_file(file).await;
                     }));
                 }
             }),
@@ -917,32 +898,65 @@ impl RoomHistory {
         dialog.show();
     }
 
-    async fn read_file(&self, file: &gio::File) {
-        let filename = file
-            .basename()
-            .unwrap()
-            .into_os_string()
-            .to_str()
-            .unwrap()
-            .to_string();
+    async fn send_file(&self, file: gio::File) {
+        let attributes: &[&str] = &[
+            *gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
+            *gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
+        ];
 
         // Read mime type.
-        let mime = if let Ok(file_info) = file.query_info(
-            "standard::content-type",
-            gio::FileQueryInfoFlags::NONE,
-            gio::Cancellable::NONE,
-        ) {
-            file_info
-                .content_type()
-                .map_or("text/plain".to_string(), |x| x.to_string())
-        } else {
-            "text/plain".to_string()
-        };
-        let mime = mime::Mime::from_str(&mime).unwrap();
+        let info = file
+            .query_info_future(
+                &attributes.join(","),
+                gio::FileQueryInfoFlags::NONE,
+                glib::PRIORITY_DEFAULT,
+            )
+            .await
+            .ok();
+
+        let mime = info
+            .as_ref()
+            .and_then(|info| info.content_type())
+            .and_then(|content_type| mime::Mime::from_str(&content_type).ok())
+            .unwrap_or(mime::APPLICATION_OCTET_STREAM);
+        let filename = info.map(|info| info.display_name()).map_or_else(
+            || filename_for_mime(Some(mime.as_ref()), None),
+            |name| name.to_string(),
+        );
 
         match file.load_contents_future().await {
-            Ok((bytes, _tag)) => self.open_attach_dialog(bytes, mime, &filename),
-            Err(err) => log::debug!("Could not read file: {}", err),
+            Ok((bytes, _tag)) => {
+                let window = self.root().unwrap().downcast::<gtk::Window>().unwrap();
+                let dialog = AttachmentDialog::for_file(&window, &filename, &file);
+
+                dialog.connect_local(
+                    "send",
+                    false,
+                    clone!(@weak self as obj => @default-return None, move |_| {
+                        if let Some(room) = obj.room() {
+                            room.send_attachment(
+                                bytes.clone(),
+                                mime.clone(),
+                                &filename,
+                            );
+                        }
+
+                        None
+                    }),
+                );
+                dialog.present();
+            }
+            Err(err) => {
+                warn!("Could not read file: {}", err);
+
+                if let Some(window) = self
+                    .root()
+                    .as_ref()
+                    .and_then(|root| root.downcast_ref::<Window>())
+                {
+                    window.add_toast(&Toast::new(&gettext("Error reading file")));
+                }
+            }
         }
     }
 
@@ -962,19 +976,3 @@ impl Default for RoomHistory {
         Self::new()
     }
 }
-
-async fn read_stream(stream: &gio::InputStream) -> Result<Vec<u8>, glib::Error> {
-    let mut buffer = Vec::<u8>::with_capacity(4096);
-
-    loop {
-        let bytes = stream
-            .read_bytes_future(4096, glib::PRIORITY_DEFAULT)
-            .await?;
-        if bytes.is_empty() {
-            break;
-        }
-        buffer.extend_from_slice(&bytes);
-    }
-
-    Ok(buffer)
-}
diff --git a/src/session/room/mod.rs b/src/session/room/mod.rs
index c0d586900..2701001a4 100644
--- a/src/session/room/mod.rs
+++ b/src/session/room/mod.rs
@@ -10,7 +10,7 @@ mod reaction_list;
 mod room_type;
 mod timeline;
 
-use std::{cell::RefCell, convert::TryInto, ops::Deref, path::PathBuf, sync::Arc};
+use std::{cell::RefCell, convert::TryInto, path::PathBuf, sync::Arc};
 
 use gettextrs::gettext;
 use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*};
@@ -1513,21 +1513,21 @@ impl Room {
         Some(())
     }
 
-    pub fn send_attachment(&self, bytes: &glib::Bytes, mime: mime::Mime, body: &str) {
+    pub fn send_attachment(&self, bytes: Vec<u8>, mime: mime::Mime, body: &str) {
         let matrix_room = self.matrix_room();
 
         if let MatrixRoom::Joined(matrix_room) = matrix_room {
             let body = body.to_string();
-            spawn_tokio!(glib::clone!(@strong bytes => async move {
+            spawn_tokio!(async move {
                 let config = AttachmentConfig::default();
-                let mut cursor = std::io::Cursor::new(bytes.deref());
+                let mut cursor = std::io::Cursor::new(&bytes);
                 matrix_room
                     // TODO This should be added to pending messages instead of
                     // sending it directly.
                     .send_attachment(&body, &mime, &mut cursor, config)
                     .await
                     .unwrap();
-            }));
+            });
         }
     }
 


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