[fractal/fractal-next] content: Add MessageFile widget
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal/fractal-next] content: Add MessageFile widget
- Date: Tue, 16 Nov 2021 15:26:36 +0000 (UTC)
commit b3e3a7c5f7edff2e403caf7e05b97abef77c8198
Author: Kévin Commaille <zecakeh tedomum fr>
Date: Tue Nov 16 16:19:15 2021 +0100
content: Add MessageFile widget
Display m.file messages so the user can open or save them.
data/resources/resources.gresource.xml | 1 +
data/resources/ui/content-message-file.ui | 41 ++++++++
po/POTFILES.in | 2 +
src/matrix_error.rs | 1 +
src/meson.build | 1 +
src/session/content/item_row.rs | 151 +++++++++++++++++++++++++++++-
src/session/content/message_row/file.rs | 111 ++++++++++++++++++++++
src/session/content/message_row/mod.rs | 9 +-
src/session/content/mod.rs | 1 -
src/session/room/event.rs | 7 +-
src/utils.rs | 19 +++-
11 files changed, 335 insertions(+), 9 deletions(-)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index a2c82cc5..cea9d767 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -11,6 +11,7 @@
<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-message-row.ui">ui/content-message-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="content-divider-row.ui">ui/content-divider-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="content-room-details.ui">ui/content-room-details.ui</file>
diff --git a/data/resources/ui/content-message-file.ui b/data/resources/ui/content-message-file.ui
new file mode 100644
index 00000000..5dc22bfe
--- /dev/null
+++ b/data/resources/ui/content-message-file.ui
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="ContentMessageFile" parent="AdwBin">
+ <property name="focusable">True</property>
+ <property name="valign">center</property>
+ <child>
+ <object class="GtkBox">
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="ellipsize">end</property>
+ <binding name="label">
+ <lookup name="filename">ContentMessageFile</lookup>
+ </binding>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <child>
+ <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>
+ </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>
+ </object>
+ </child>
+ <style>
+ <class name="linked"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 09cca574..1e9de487 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -18,6 +18,7 @@ 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
+data/resources/ui/content-message-file.ui
data/resources/ui/content-message-row.ui
data/resources/ui/content-room-details.ui
data/resources/ui/content-room-history.ui
@@ -74,6 +75,7 @@ src/session/content/divider_row.rs
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/mod.rs
src/session/content/message_row/text.rs
src/session/content/mod.rs
diff --git a/src/matrix_error.rs b/src/matrix_error.rs
index ab6324ab..3a66fd3b 100644
--- a/src/matrix_error.rs
+++ b/src/matrix_error.rs
@@ -47,6 +47,7 @@ impl UserFacingError for HttpError {
impl UserFacingError for Error {
fn to_user_facing(self) -> String {
match self {
+ Error::DecryptorError(_) => gettext("Could not decrypt the event"),
Error::Http(http_error) => http_error.to_user_facing(),
_ => gettext("An unknown error occurred."),
}
diff --git a/src/meson.build b/src/meson.build
index 5769b779..6a9f04cb 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -57,6 +57,7 @@ sources = files(
'session/content/item_row.rs',
'session/content/invite.rs',
'session/content/markdown_popover.rs',
+ 'session/content/message_row/file.rs',
'session/content/message_row/mod.rs',
'session/content/message_row/text.rs',
'session/content/mod.rs',
diff --git a/src/session/content/item_row.rs b/src/session/content/item_row.rs
index 50b340b9..1d6a7c43 100644
--- a/src/session/content/item_row.rs
+++ b/src/session/content/item_row.rs
@@ -1,12 +1,18 @@
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
-use gtk::{gio, glib, glib::clone, subclass::prelude::*};
+use gtk::{gio, glib, glib::clone, subclass::prelude::*, FileChooserAction, ResponseType};
+use log::error;
+use matrix_sdk::ruma::events::{
+ room::message::MessageType, AnyMessageEventContent, AnySyncRoomEvent,
+};
use crate::components::{ContextMenuBin, ContextMenuBinExt, ContextMenuBinImpl};
-use crate::session::content::{DividerRow, MessageRow, StateRow};
+use crate::matrix_error::UserFacingError;
+use crate::session::content::{message_row::MessageRow, DividerRow, StateRow};
use crate::session::event_source_dialog::EventSourceDialog;
use crate::session::room::{Event, Item, ItemType};
-use matrix_sdk::ruma::events::AnySyncRoomEvent;
+use crate::utils::cache_dir;
+use crate::{spawn, spawn_tokio, Error, Window};
mod imp {
use super::*;
@@ -34,6 +40,26 @@ mod imp {
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;
+ })
+ );
+ });
}
}
@@ -240,6 +266,125 @@ 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 {
diff --git a/src/session/content/message_row/file.rs b/src/session/content/message_row/file.rs
new file mode 100644
index 00000000..eaa317dc
--- /dev/null
+++ b/src/session/content/message_row/file.rs
@@ -0,0 +1,111 @@
+use adw::subclass::prelude::*;
+use gtk::{glib, prelude::*, subclass::prelude::*, CompositeTemplate};
+
+mod imp {
+ use super::*;
+ use glib::subclass::InitializingObject;
+ use once_cell::sync::Lazy;
+ use std::cell::RefCell;
+
+ #[derive(Debug, Default, CompositeTemplate)]
+ #[template(resource = "/org/gnome/FractalNext/content-message-file.ui")]
+ pub struct MessageFile {
+ /// The filename of the file
+ pub filename: RefCell<Option<String>>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for MessageFile {
+ const NAME: &'static str = "ContentMessageFile";
+ type Type = super::MessageFile;
+ 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 MessageFile {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![glib::ParamSpec::new_string(
+ "filename",
+ "Filename",
+ "The filename of the file",
+ None,
+ glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+ )]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "filename" => obj.set_filename(value.get().unwrap()),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "filename" => obj.filename().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn constructed(&self, obj: &Self::Type) {
+ self.parent_constructed(obj);
+ }
+ }
+
+ impl WidgetImpl for MessageFile {}
+
+ impl BinImpl for MessageFile {}
+}
+
+glib::wrapper! {
+ /// A widget displaying an interface to download or open the content of a file message.
+ pub struct MessageFile(ObjectSubclass<imp::MessageFile>)
+ @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+impl MessageFile {
+ pub fn new(filename: Option<String>) -> Self {
+ glib::Object::new(&[("filename", &filename)]).expect("Failed to create MessageFile")
+ }
+
+ pub fn set_filename(&self, filename: Option<String>) {
+ let priv_ = imp::MessageFile::from_instance(self);
+
+ let name = filename.filter(|name| !name.is_empty());
+
+ if name.as_ref() == priv_.filename.borrow().as_ref() {
+ return;
+ }
+
+ priv_.filename.replace(name);
+ self.notify("filename");
+ }
+
+ pub fn filename(&self) -> Option<String> {
+ let priv_ = imp::MessageFile::from_instance(self);
+ priv_.filename.borrow().to_owned()
+ }
+}
+
+impl Default for MessageFile {
+ fn default() -> Self {
+ Self::new(None)
+ }
+}
diff --git a/src/session/content/message_row/mod.rs b/src/session/content/message_row/mod.rs
index a31d611a..461e01c3 100644
--- a/src/session/content/message_row/mod.rs
+++ b/src/session/content/message_row/mod.rs
@@ -1,3 +1,4 @@
+mod file;
mod text;
use crate::components::Avatar;
@@ -13,7 +14,7 @@ use matrix_sdk::ruma::events::{
AnyMessageEventContent, AnySyncMessageEvent, AnySyncRoomEvent,
};
-use self::text::MessageText;
+use self::{file::MessageFile, text::MessageText};
use crate::prelude::*;
use crate::session::room::Event;
@@ -261,7 +262,11 @@ impl MessageRow {
MessageText::emote(message.formatted, message.body, event.sender());
priv_.content.set_child(Some(&child));
}
- MessageType::File(_message) => {}
+ MessageType::File(message) => {
+ let filename = message.filename.unwrap_or(message.body);
+ let child = MessageFile::new(Some(filename));
+ priv_.content.set_child(Some(&child));
+ }
MessageType::Image(_message) => {}
MessageType::Location(_message) => {}
MessageType::Notice(message) => {
diff --git a/src/session/content/mod.rs b/src/session/content/mod.rs
index d07515ee..12bf3fb5 100644
--- a/src/session/content/mod.rs
+++ b/src/session/content/mod.rs
@@ -15,7 +15,6 @@ use self::explore::Explore;
use self::invite::Invite;
use self::item_row::ItemRow;
use self::markdown_popover::MarkdownPopover;
-use self::message_row::MessageRow;
use self::room_details::RoomDetails;
use self::room_history::RoomHistory;
use self::state_row::StateRow;
diff --git a/src/session/room/event.rs b/src/session/room/event.rs
index bc6f352e..cfe60c79 100644
--- a/src/session/room/event.rs
+++ b/src/session/room/event.rs
@@ -137,7 +137,7 @@ mod imp {
match pspec.name() {
"source" => obj.source().to_value(),
"sender" => obj.sender().to_value(),
- "room" => self.room.get().unwrap().to_value(),
+ "room" => obj.room().to_value(),
"show-header" => obj.show_header().to_value(),
"can-hide-header" => obj.can_hide_header().to_value(),
"time" => obj.time().to_value(),
@@ -170,6 +170,11 @@ impl Event {
.member_by_id(&self.matrix_sender())
}
+ pub fn room(&self) -> &Room {
+ let priv_ = imp::Event::from_instance(self);
+ priv_.room.get().unwrap()
+ }
+
/// Get the matrix event
///
/// If the `SyncRoomEvent` couldn't be deserialized this is `None`
diff --git a/src/utils.rs b/src/utils.rs
index 19a6db3e..e4b336a2 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -58,8 +58,10 @@ macro_rules! spawn_tokio {
};
}
-use gtk::gio::prelude::*;
-use gtk::glib::Object;
+use std::path::PathBuf;
+
+use gtk::gio::{self, prelude::*};
+use gtk::glib::{self, Object};
/// Returns an expression looking up the given property on `object`.
pub fn prop_expr<T: IsA<Object>>(object: &T, prop: &str) -> gtk::Expression {
@@ -106,3 +108,16 @@ pub fn not_expr(a_expr: gtk::Expression) -> gtk::Expression {
)
.upcast()
}
+
+pub fn cache_dir() -> PathBuf {
+ let mut path = glib::user_cache_dir();
+ path.push("fractal");
+
+ if !path.exists() {
+ let dir = gio::File::for_path(path.clone());
+ dir.make_directory_with_parents(gio::NONE_CANCELLABLE)
+ .unwrap();
+ }
+
+ path
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]