content: Add MessageFile widget

Display m.file messages so the user can open or save them.
This commit is contained in:
Kévin Commaille 2021-11-16 16:19:15 +01:00
parent b653ca7933
commit b3e3a7c5f7
No known key found for this signature in database
GPG key ID: DD507DAE96E8245C
11 changed files with 335 additions and 9 deletions

View file

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

View file

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

View file

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

View file

@ -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."),
}

View file

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

View file

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

View file

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

View file

@ -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) => {

View file

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

View file

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

View file

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