content: Move ItemRow's Event actions to its own trait

This will allow to use the same actions on other widgets.
This commit is contained in:
Kévin Commaille 2021-11-30 14:36:56 +01:00
parent e2da077328
commit a92c21770a
No known key found for this signature in database
GPG key ID: DD507DAE96E8245C
10 changed files with 251 additions and 203 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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