[fractal/fractal-next] content: Move ItemRow's Event actions to its own trait



commit a92c21770a07066c837ef8886fc03cb26123ffa1
Author: Kévin Commaille <zecakeh tedomum fr>
Date:   Tue Nov 30 14:36:56 2021 +0100

    content: Move ItemRow's Event actions to its own trait
    
    This will allow to use the same actions on other widgets.

 data/resources/resources.gresource.xml             |   2 +-
 data/resources/ui/content-message-file.ui          |   4 +-
 .../ui/{content-item-row-menu.ui => event-menu.ui} |  23 ++-
 po/POTFILES.in                                     |   3 +-
 src/meson.build                                    |   1 +
 src/session/content/room_history/item_row.rs       | 185 +--------------------
 .../content/room_history/message_row/text.rs       |  14 +-
 src/session/room/event.rs                          |  38 ++++-
 src/session/room/event_actions.rs                  | 182 ++++++++++++++++++++
 src/session/room/mod.rs                            |   2 +
 10 files changed, 251 insertions(+), 203 deletions(-)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 0aa5e481..607d1544 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -10,7 +10,6 @@
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-explore-item.ui">ui/content-explore-item.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-public-room-row.ui">ui/content-public-room-row.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="content-item.ui">ui/content-item.ui</file>
-    <file compressed="true" preprocess="xml-stripblanks" 
alias="content-item-row-menu.ui">ui/content-item-row-menu.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-message-file.ui">ui/content-message-file.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-member-page.ui">ui/content-member-page.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-member-row.ui">ui/content-member-row.ui</file>
@@ -20,6 +19,7 @@
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-state-row.ui">ui/content-state-row.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-markdown-popover.ui">ui/content-markdown-popover.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-invite.ui">ui/content-invite.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" alias="event-menu.ui">ui/event-menu.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="event-source-dialog.ui">ui/event-source-dialog.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="login.ui">ui/login.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="session.ui">ui/session.ui</file>
diff --git a/data/resources/ui/content-message-file.ui b/data/resources/ui/content-message-file.ui
index 5dc22bfe..fc7dff38 100644
--- a/data/resources/ui/content-message-file.ui
+++ b/data/resources/ui/content-message-file.ui
@@ -20,14 +20,14 @@
                 <object class="GtkButton" id="open">
                   <property name="icon-name">document-open-symbolic</property>
                   <property name="tooltip-text" translatable="yes">Open</property>
-                  <property name="action-name">item-row.file-open</property>
+                  <property name="action-name">event.file-open</property>
                 </object>
               </child>
               <child>
                 <object class="GtkButton" id="save">
                   <property name="icon-name">document-save-symbolic</property>
                   <property name="tooltip-text" translatable="yes">Save</property>
-                  <property name="action-name">item-row.file-save</property>
+                  <property name="action-name">event.file-save</property>
                 </object>
               </child>
               <style>
diff --git a/data/resources/ui/content-item-row-menu.ui b/data/resources/ui/event-menu.ui
similarity index 71%
rename from data/resources/ui/content-item-row-menu.ui
rename to data/resources/ui/event-menu.ui
index 54e3c418..30c68073 100644
--- a/data/resources/ui/content-item-row-menu.ui
+++ b/data/resources/ui/event-menu.ui
@@ -4,64 +4,61 @@
     <section>
       <item>
         <attribute name="label" translatable="yes">_Reply</attribute>
-        <attribute name="action">item-row.reply</attribute>
+        <attribute name="action">event.reply</attribute>
         <attribute name="hidden-when">action-missing</attribute>
       </item>
       <item>
         <attribute name="label" translatable="yes">_Edit</attribute>
-        <attribute name="action">item-row.edit</attribute>
+        <attribute name="action">event.edit</attribute>
         <attribute name="hidden-when">action-missing</attribute>
       </item>
       <item>
         <attribute name="label" translatable="yes">_Forward</attribute>
-        <attribute name="action">item-row.forward</attribute>
+        <attribute name="action">event.forward</attribute>
         <attribute name="hidden-when">action-missing</attribute>
       </item>
     </section>
     <section>
       <item>
         <attribute name="label" translatable="yes">_Select</attribute>
-        <attribute name="action">item-row.select</attribute>
+        <attribute name="action">event.select</attribute>
         <attribute name="hidden-when">action-missing</attribute>
       </item>
     </section>
     <section>
       <item>
         <attribute name="label" translatable="yes">_Copy Text</attribute>
-        <attribute name="action">item-row.copy-text</attribute>
+        <attribute name="action">event.copy-text</attribute>
         <attribute name="hidden-when">action-disabled</attribute>
         <attribute name="hidden-when">action-missing</attribute>
       </item>
       <item>
         <attribute name="label" translatable="yes">_Copy Image</attribute>
-        <attribute name="action">item-row.copy-image</attribute>
-        <attribute name="hidden-when">action-disabled</attribute>
+        <attribute name="action">event.copy-image</attribute>
         <attribute name="hidden-when">action-missing</attribute>
       </item>
       <item>
         <attribute name="label" translatable="yes">S_ave Image</attribute>
-        <attribute name="action">item-row.save-image</attribute>
-        <attribute name="hidden-when">action-disabled</attribute>
+        <attribute name="action">event.save-image</attribute>
         <attribute name="hidden-when">action-missing</attribute>
       </item>
       <item>
         <attribute name="label" translatable="yes">_Permalink</attribute>
-        <attribute name="action">item-row.permalink</attribute>
+        <attribute name="action">event.permalink</attribute>
         <attribute name="hidden-when">action-missing</attribute>
       </item>
       <item>
         <attribute name="label" translatable="yes">_View Source</attribute>
-        <attribute name="action">item-row.view-source</attribute>
+        <attribute name="action">event.view-source</attribute>
         <attribute name="hidden-when">action-missing</attribute>
       </item>
     </section>
     <section>
       <item>
         <attribute name="label" translatable="yes">Re_move</attribute>
-        <attribute name="action">item-row.remove</attribute>
+        <attribute name="action">event.remove</attribute>
         <attribute name="hidden-when">action-missing</attribute>
       </item>
     </section>
   </menu>
 </interface>
-
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 3f25f606..c47338ca 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -14,7 +14,6 @@ data/resources/ui/components-avatar.ui
 data/resources/ui/components-loading-listbox-row.ui
 data/resources/ui/avatar-with-selection.ui
 data/resources/ui/content-divider-row.ui
-data/resources/ui/content-item-row-menu.ui
 data/resources/ui/content-item.ui
 data/resources/ui/content-invite.ui
 data/resources/ui/content-markdown-popover.ui
@@ -25,6 +24,7 @@ data/resources/ui/content-room-history.ui
 data/resources/ui/content-state-row.ui
 data/resources/ui/content.ui
 data/resources/ui/context-menu-bin.ui
+data/resources/ui/event-menu.ui
 data/resources/ui/event-source-dialog.ui
 data/resources/ui/login.ui
 data/resources/ui/in-app-notification.ui
@@ -89,6 +89,7 @@ src/session/content/room_history/state_row.rs
 src/session/mod.rs
 src/session/room_creation/mod.rs
 src/session/room_list.rs
+src/session/room/event_actions.rs
 src/session/room/event.rs
 src/session/room/highlight_flags.rs
 src/session/room/item.rs
diff --git a/src/meson.build b/src/meson.build
index d3ccd1cd..ccc4245e 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -71,6 +71,7 @@ sources = files(
   'session/content/mod.rs',
   'session/content/room_details/member_page.rs',
   'session/content/room_details/mod.rs',
+  'session/room/event_actions.rs',
   'session/room/event.rs',
   'session/room/highlight_flags.rs',
   'session/room/item.rs',
diff --git a/src/session/content/room_history/item_row.rs b/src/session/content/room_history/item_row.rs
index 8b013544..5f48008f 100644
--- a/src/session/content/room_history/item_row.rs
+++ b/src/session/content/room_history/item_row.rs
@@ -1,18 +1,11 @@
 use adw::{prelude::*, subclass::prelude::*};
 use gettextrs::gettext;
-use gtk::{gio, glib, glib::clone, subclass::prelude::*, FileChooserAction, ResponseType};
-use log::error;
-use matrix_sdk::ruma::events::{
-    room::message::MessageType, AnyMessageEventContent, AnySyncRoomEvent,
-};
+use gtk::{gio, glib, glib::clone, subclass::prelude::*};
+use matrix_sdk::ruma::events::AnySyncRoomEvent;
 
 use crate::components::{ContextMenuBin, ContextMenuBinExt, ContextMenuBinImpl};
-use crate::matrix_error::UserFacingError;
 use crate::session::content::room_history::{message_row::MessageRow, DividerRow, StateRow};
-use crate::session::event_source_dialog::EventSourceDialog;
-use crate::session::room::{Event, Item, ItemType};
-use crate::utils::cache_dir;
-use crate::{spawn, spawn_tokio, Error, Window};
+use crate::session::room::{Event, EventActions, Item, ItemType};
 
 mod imp {
     use super::*;
@@ -31,36 +24,6 @@ mod imp {
         const NAME: &'static str = "ContentItemRow";
         type Type = super::ItemRow;
         type ParentType = ContextMenuBin;
-
-        fn class_init(klass: &mut Self::Class) {
-            // View Event Source
-            klass.install_action("item-row.view-source", None, move |widget, _, _| {
-                let window = widget.root().unwrap().downcast().unwrap();
-                let dialog =
-                    EventSourceDialog::new(&window, widget.item().unwrap().event().unwrap());
-                dialog.show();
-            });
-
-            // Save message's file
-            klass.install_action("item-row.file-save", None, move |widget, _, _| {
-                spawn!(
-                    glib::PRIORITY_LOW,
-                    clone!(@weak widget as obj => async move {
-                        obj.save_file().await;
-                    })
-                );
-            });
-
-            // Open message's file
-            klass.install_action("item-row.file-open", None, move |widget, _, _| {
-                spawn!(
-                    glib::PRIORITY_LOW,
-                    clone!(@weak widget as obj => async move {
-                        obj.open_file().await;
-                    })
-                );
-            });
-        }
     }
 
     impl ObjectImpl for ItemRow {
@@ -136,14 +99,6 @@ impl ItemRow {
         priv_.item.borrow().clone()
     }
 
-    fn enable_gactions(&self) {
-        self.action_set_enabled("item-row.view-source", true);
-    }
-
-    fn disable_gactions(&self) {
-        self.action_set_enabled("item-row.view-source", false);
-    }
-
     /// This method sets this row to a new `Item`.
     ///
     /// It tries to reuse the widget and only update the content whenever possible, but it will
@@ -162,14 +117,9 @@ impl ItemRow {
             match item.type_() {
                 ItemType::Event(event) => {
                     if self.context_menu().is_none() {
-                        let menu_model = gtk::Builder::from_resource(
-                            "/org/gnome/FractalNext/content-item-row-menu.ui",
-                        )
-                        .object("menu_model");
-                        self.set_context_menu(menu_model);
-
-                        self.enable_gactions();
+                        self.set_context_menu(Some(Self::event_menu_model()));
                     }
+                    self.set_event_actions(Some(event));
 
                     let event_notify_handler = event.connect_notify_local(
                         Some("event"),
@@ -188,7 +138,7 @@ impl ItemRow {
                 ItemType::DayDivider(date) => {
                     if self.context_menu().is_some() {
                         self.set_context_menu(None);
-                        self.disable_gactions();
+                        self.set_event_actions(None);
                     }
 
                     let fmt = if date.year() == glib::DateTime::new_now_local().unwrap().year() {
@@ -210,7 +160,7 @@ impl ItemRow {
                 ItemType::NewMessageDivider => {
                     if self.context_menu().is_some() {
                         self.set_context_menu(None);
-                        self.disable_gactions();
+                        self.set_event_actions(None);
                     }
 
                     let label = gettext("New Messages");
@@ -266,125 +216,6 @@ impl ItemRow {
             }
         }
     }
-
-    pub async fn save_file(&self) {
-        let (filename, data) = match self.get_media_content().await {
-            Ok(res) => res,
-            Err(err) => {
-                error!("Could not get file: {}", err);
-
-                let error_message = err.to_user_facing();
-                let error = Error::new(move |_| {
-                    let error_label = gtk::LabelBuilder::new()
-                        .label(&error_message)
-                        .wrap(true)
-                        .build();
-                    Some(error_label.upcast())
-                });
-                if let Some(window) = self.root().and_then(|root| root.downcast::<Window>().ok()) {
-                    window.append_error(&error);
-                }
-
-                return;
-            }
-        };
-
-        let window: gtk::Window = self.root().unwrap().downcast().unwrap();
-        let dialog = gtk::FileChooserDialog::new(
-            Some(&gettext("Save File")),
-            Some(&window),
-            FileChooserAction::Save,
-            &[
-                (&gettext("Save"), ResponseType::Accept),
-                (&gettext("Cancel"), ResponseType::Cancel),
-            ],
-        );
-        dialog.set_current_name(&filename);
-
-        let response = dialog.run_future().await;
-        if response == ResponseType::Accept {
-            if let Some(file) = dialog.file() {
-                file.replace_contents(
-                    &data,
-                    None,
-                    false,
-                    gio::FileCreateFlags::REPLACE_DESTINATION,
-                    gio::NONE_CANCELLABLE,
-                )
-                .unwrap();
-            }
-        }
-
-        dialog.close();
-    }
-
-    pub async fn open_file(&self) {
-        let (filename, data) = match self.get_media_content().await {
-            Ok(res) => res,
-            Err(err) => {
-                error!("Could not get file: {}", err);
-
-                let error_message = err.to_user_facing();
-                let error = Error::new(move |_| {
-                    let error_label = gtk::LabelBuilder::new()
-                        .label(&error_message)
-                        .wrap(true)
-                        .build();
-                    Some(error_label.upcast())
-                });
-                if let Some(window) = self.root().and_then(|root| root.downcast::<Window>().ok()) {
-                    window.append_error(&error);
-                }
-
-                return;
-            }
-        };
-
-        let mut path = cache_dir();
-        path.push(filename);
-        let file = gio::File::for_path(path);
-
-        file.replace_contents(
-            &data,
-            None,
-            false,
-            gio::FileCreateFlags::REPLACE_DESTINATION,
-            gio::NONE_CANCELLABLE,
-        )
-        .unwrap();
-
-        if let Err(error) = gio::AppInfo::launch_default_for_uri_async_future(
-            &file.uri(),
-            gio::NONE_APP_LAUNCH_CONTEXT,
-        )
-        .await
-        {
-            error!("Error opening file '{}': {}", file.uri(), error);
-        }
-    }
-
-    async fn get_media_content(&self) -> Result<(String, Vec<u8>), matrix_sdk::Error> {
-        let item = self.item().unwrap();
-        let event = item.event().unwrap();
-
-        if let AnySyncRoomEvent::Message(message_event) = event.matrix_event().unwrap() {
-            if let AnyMessageEventContent::RoomMessage(content) = message_event.content() {
-                let client = event.room().session().client();
-                match content.msgtype {
-                    MessageType::File(file_content) => {
-                        let content = file_content.clone();
-                        let handle =
-                            spawn_tokio!(async move { client.get_file(content, true).await });
-                        let data = handle.await.unwrap()?.unwrap();
-                        return Ok((file_content.filename.unwrap_or(file_content.body), data));
-                    }
-                    _ => {}
-                };
-            }
-        };
-
-        panic!("Trying to get the media content of an event of incompatible type");
-    }
 }
 
 impl Default for ItemRow {
@@ -392,3 +223,5 @@ impl Default for ItemRow {
         Self::new()
     }
 }
+
+impl EventActions for ItemRow {}
diff --git a/src/session/content/room_history/message_row/text.rs 
b/src/session/content/room_history/message_row/text.rs
index 3117ead3..c28ce502 100644
--- a/src/session/content/room_history/message_row/text.rs
+++ b/src/session/content/room_history/message_row/text.rs
@@ -1,5 +1,5 @@
 use adw::{prelude::BinExt, subclass::prelude::*};
-use gtk::{gio, glib, pango, prelude::*, subclass::prelude::*};
+use gtk::{glib, pango, prelude::*, subclass::prelude::*};
 use html2pango::{
     block::{markup_html, HtmlBlock},
     html_escape, markup_links,
@@ -7,7 +7,11 @@ use html2pango::{
 use matrix_sdk::ruma::events::room::message::{FormattedBody, MessageFormat};
 use sourceview::prelude::*;
 
-use crate::session::{room::Member, UserExt};
+use crate::session::{
+    content::room_history::ItemRow,
+    room::{EventActions, Member},
+    UserExt,
+};
 
 #[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::GEnum)]
 #[repr(u32)]
@@ -320,10 +324,8 @@ fn set_label_styles(w: &gtk::Label) {
     w.set_valign(gtk::Align::Start);
     w.set_halign(gtk::Align::Fill);
     w.set_selectable(true);
-    let menu_model: Option<gio::MenuModel> =
-        gtk::Builder::from_resource("/org/gnome/FractalNext/content-item-row-menu.ui")
-            .object("menu_model");
-    w.set_extra_menu(menu_model.as_ref());
+    let menu_model = ItemRow::event_menu_model();
+    w.set_extra_menu(Some(&menu_model));
 }
 
 fn create_widget_for_html_block(block: &HtmlBlock) -> gtk::Widget {
diff --git a/src/session/room/event.rs b/src/session/room/event.rs
index abe6c0f9..57440472 100644
--- a/src/session/room/event.rs
+++ b/src/session/room/event.rs
@@ -1,4 +1,5 @@
 use gtk::{glib, glib::DateTime, prelude::*, subclass::prelude::*};
+use log::warn;
 use matrix_sdk::{
     deserialized_responses::SyncRoomEvent,
     ruma::{
@@ -12,9 +13,10 @@ use matrix_sdk::{
     },
 };
 
-use crate::session::room::Member;
-use crate::session::Room;
-use log::warn;
+use crate::{
+    session::{room::Member, Room},
+    spawn_tokio,
+};
 
 #[derive(Clone, Debug, glib::GBoxed)]
 #[gboxed(type_name = "BoxedSyncRoomEvent")]
@@ -438,7 +440,10 @@ impl Event {
         priv_.show_header.get()
     }
 
-    fn message_content(&self) -> Option<AnyMessageEventContent> {
+    /// The content of this message.
+    ///
+    /// Returns `None` if this is not a message.
+    pub fn message_content(&self) -> Option<AnyMessageEventContent> {
         match self.matrix_event() {
             Some(AnySyncRoomEvent::Message(message)) => Some(message.content()),
             _ => None,
@@ -495,4 +500,29 @@ impl Event {
     ) -> glib::SignalHandlerId {
         self.connect_notify_local(Some("show-header"), f)
     }
+
+    /// The content of a media message.
+    ///
+    /// Compatible events:
+    ///
+    /// - File message (`MessageType::File`).
+    ///
+    /// Returns `Ok((filename, binary_content))` on success, `Err` if an error occured while
+    /// fetching the content. Panics on an incompatible event.
+    pub async fn get_media_content(&self) -> Result<(String, Vec<u8>), matrix_sdk::Error> {
+        if let AnyMessageEventContent::RoomMessage(content) = self.message_content().unwrap() {
+            let client = self.room().session().client();
+            match content.msgtype {
+                MessageType::File(file_content) => {
+                    let content = file_content.clone();
+                    let handle = spawn_tokio!(async move { client.get_file(content, true).await });
+                    let data = handle.await.unwrap()?.unwrap();
+                    return Ok((file_content.filename.unwrap_or(file_content.body), data));
+                }
+                _ => {}
+            };
+        };
+
+        panic!("Trying to get the media content of an event of incompatible type");
+    }
 }
diff --git a/src/session/room/event_actions.rs b/src/session/room/event_actions.rs
new file mode 100644
index 00000000..ba93fe0e
--- /dev/null
+++ b/src/session/room/event_actions.rs
@@ -0,0 +1,182 @@
+use gettextrs::gettext;
+use gtk::{gio, glib, glib::clone, prelude::*};
+use log::error;
+use matrix_sdk::ruma::events::{room::message::MessageType, AnyMessageEventContent};
+
+use crate::{
+    matrix_error::UserFacingError,
+    session::{event_source_dialog::EventSourceDialog, room::Event},
+    spawn,
+    utils::cache_dir,
+    Error, Window,
+};
+
+pub trait EventActions
+where
+    Self: IsA<gtk::Widget>,
+    Self: glib::clone::Downgrade,
+    <Self as glib::clone::Downgrade>::Weak: glib::clone::Upgrade<Strong = Self>,
+{
+    /// The `MenuModel` for common event actions.
+    fn event_menu_model() -> gio::MenuModel {
+        gtk::Builder::from_resource("/org/gnome/FractalNext/event-menu.ui")
+            .object("menu_model")
+            .unwrap()
+    }
+
+    /// Set the actions available on `self` for `event`.
+    ///
+    /// Unsets the actions if `event` is `None`.
+    ///
+    /// Should be used with the compatible model from `event_menu_model`.
+    fn set_event_actions(&self, event: Option<&Event>) {
+        if event.is_none() {
+            self.insert_action_group("event", gio::NONE_ACTION_GROUP);
+        }
+
+        let event = event.unwrap();
+        let action_group = gio::SimpleActionGroup::new();
+
+        // View Event Source
+        let view_source = gio::SimpleAction::new("view-source", None);
+        view_source.connect_activate(clone!(@weak self as widget, @weak event => move |_, _| {
+            let window = widget.root().unwrap().downcast().unwrap();
+            let dialog = EventSourceDialog::new(&window, &event);
+            dialog.show();
+        }));
+        action_group.add_action(&view_source);
+
+        if let Some(AnyMessageEventContent::RoomMessage(message)) = event.message_content() {
+            if let MessageType::File(_) = message.msgtype {
+                // Save message's file
+                let file_save = gio::SimpleAction::new("file-save", None);
+                file_save.connect_activate(
+                    clone!(@weak self as widget, @weak event => move |_, _| {
+                        widget.save_event_file(event);
+                    }),
+                );
+                action_group.add_action(&file_save);
+
+                // Open message's file
+                let file_open = gio::SimpleAction::new("file-open", None);
+                file_open.connect_activate(
+                    clone!(@weak self as widget, @weak event => move |_, _| {
+                        widget.open_event_file(event);
+                    }),
+                );
+                action_group.add_action(&file_open);
+            }
+        }
+
+        self.insert_action_group("event", Some(&action_group));
+    }
+
+    /// Save the file in `event`.
+    ///
+    /// See `Event::get_media_content` for compatible events. Panics on an incompatible event.
+    fn save_event_file(&self, event: Event) {
+        let window: Window = self.root().unwrap().downcast().unwrap();
+        spawn!(
+            glib::PRIORITY_LOW,
+            clone!(@weak window => async move {
+                let (filename, data) = match event.get_media_content().await {
+                    Ok(res) => res,
+                    Err(err) => {
+                        error!("Could not get file: {}", err);
+
+                        let error_message = err.to_user_facing();
+                        let error = Error::new(move |_| {
+                            let error_label = gtk::LabelBuilder::new()
+                                .label(&error_message)
+                                .wrap(true)
+                                .build();
+                            Some(error_label.upcast())
+                        });
+                        window.append_error(&error);
+
+                        return;
+                    }
+                };
+
+                let dialog = gtk::FileChooserDialog::new(
+                    Some(&gettext("Save File")),
+                    Some(&window),
+                    gtk::FileChooserAction::Save,
+                    &[
+                        (&gettext("Save"), gtk::ResponseType::Accept),
+                        (&gettext("Cancel"), gtk::ResponseType::Cancel),
+                    ],
+                );
+                dialog.set_current_name(&filename);
+
+                let response = dialog.run_future().await;
+                if response == gtk::ResponseType::Accept {
+                    if let Some(file) = dialog.file() {
+                        file.replace_contents(
+                            &data,
+                            None,
+                            false,
+                            gio::FileCreateFlags::REPLACE_DESTINATION,
+                            gio::NONE_CANCELLABLE,
+                        )
+                        .unwrap();
+                    }
+                }
+
+                dialog.close();
+            })
+        );
+    }
+
+    /// Open the file in `event`.
+    ///
+    /// See `Event::get_media_content` for compatible events. Panics on an incompatible event.
+    fn open_event_file(&self, event: Event) {
+        let window: Window = self.root().unwrap().downcast().unwrap();
+        spawn!(
+            glib::PRIORITY_LOW,
+            clone!(@weak window => async move {
+                let (filename, data) = match event.get_media_content().await {
+                    Ok(res) => res,
+                    Err(err) => {
+                        error!("Could not get file: {}", err);
+
+                        let error_message = err.to_user_facing();
+                        let error = Error::new(move |_| {
+                            let error_label = gtk::LabelBuilder::new()
+                                .label(&error_message)
+                                .wrap(true)
+                                .build();
+                            Some(error_label.upcast())
+                        });
+                        window.append_error(&error);
+
+                        return;
+                    }
+                };
+
+                let mut path = cache_dir();
+                path.push(filename);
+                let file = gio::File::for_path(path);
+
+                file.replace_contents(
+                    &data,
+                    None,
+                    false,
+                    gio::FileCreateFlags::REPLACE_DESTINATION,
+                    gio::NONE_CANCELLABLE,
+                )
+                .unwrap();
+
+                if let Err(error) = gio::AppInfo::launch_default_for_uri_async_future(
+                    &file.uri(),
+                    gio::NONE_APP_LAUNCH_CONTEXT,
+                )
+                .await
+                {
+                    error!("Error opening file '{}': {}", file.uri(), error);
+                }
+            })
+        );
+    }
+}
diff --git a/src/session/room/mod.rs b/src/session/room/mod.rs
index 15c6929f..d16edfc9 100644
--- a/src/session/room/mod.rs
+++ b/src/session/room/mod.rs
@@ -1,4 +1,5 @@
 mod event;
+mod event_actions;
 mod highlight_flags;
 mod item;
 mod member;
@@ -9,6 +10,7 @@ mod room_type;
 mod timeline;
 
 pub use self::event::Event;
+pub use self::event_actions::EventActions;
 pub use self::highlight_flags::HighlightFlags;
 pub use self::item::Item;
 pub use self::item::ItemType;


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