item-row: Merge with event_actions
Since it's the only file to use it, it does not make sense to implement event actions as a trait anymore.
This commit is contained in:
parent
2b46670dfb
commit
251dd38aa5
4 changed files with 330 additions and 368 deletions
|
@ -71,7 +71,6 @@ src/session/view/content/room_details/member_page/mod.rs
|
|||
src/session/view/content/room_details/member_page/mod.ui
|
||||
src/session/view/content/room_details/mod.ui
|
||||
src/session/view/content/room_history/attachment_dialog.ui
|
||||
src/session/view/content/room_history/event_actions.rs
|
||||
src/session/view/content/room_history/event_actions.ui
|
||||
src/session/view/content/room_history/item_row.rs
|
||||
src/session/view/content/room_history/message_row/audio.rs
|
||||
|
|
|
@ -1,352 +0,0 @@
|
|||
use gettextrs::gettext;
|
||||
use gtk::{gdk, gio, glib, glib::clone, prelude::*};
|
||||
use matrix_sdk_ui::timeline::TimelineItemContent;
|
||||
use once_cell::sync::Lazy;
|
||||
use ruma::events::room::{message::MessageType, power_levels::PowerLevelAction};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
prelude::*,
|
||||
session::{
|
||||
model::{Event, EventKey},
|
||||
view::EventSourceDialog,
|
||||
},
|
||||
spawn, spawn_tokio, toast,
|
||||
utils::media::save_to_file,
|
||||
};
|
||||
|
||||
// This is only safe because the trait `EventActions` can
|
||||
// only be implemented on `gtk::Widgets` that run only on the main thread
|
||||
struct MenuModelSendSync(gio::MenuModel);
|
||||
#[allow(clippy::non_send_fields_in_send_ty)]
|
||||
unsafe impl Send for MenuModelSendSync {}
|
||||
unsafe impl Sync for MenuModelSendSync {}
|
||||
|
||||
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 message event actions.
|
||||
fn event_message_menu_model() -> &'static gio::MenuModel {
|
||||
static MODEL: Lazy<MenuModelSendSync> = Lazy::new(|| {
|
||||
MenuModelSendSync(
|
||||
gtk::Builder::from_resource(
|
||||
"/org/gnome/Fractal/ui/session/view/content/room_history/event_actions.ui",
|
||||
)
|
||||
.object::<gio::MenuModel>("message_menu_model")
|
||||
.unwrap(),
|
||||
)
|
||||
});
|
||||
&MODEL.0
|
||||
}
|
||||
|
||||
/// The default `MenuModel` for common state event actions.
|
||||
fn event_state_menu_model() -> &'static gio::MenuModel {
|
||||
static MODEL: Lazy<MenuModelSendSync> = Lazy::new(|| {
|
||||
MenuModelSendSync(
|
||||
gtk::Builder::from_resource(
|
||||
"/org/gnome/Fractal/ui/session/view/content/room_history/event_actions.ui",
|
||||
)
|
||||
.object::<gio::MenuModel>("state_menu_model")
|
||||
.unwrap(),
|
||||
)
|
||||
});
|
||||
&MODEL.0
|
||||
}
|
||||
|
||||
/// Store a `GtkExpressionWatch` for re-use later.
|
||||
fn set_expression_watch(&self, key: &'static str, expr_watch: gtk::ExpressionWatch);
|
||||
|
||||
/// Get a `GtkExpressionWatch` by key.
|
||||
fn expression_watch(&self, key: &&str) -> Option<gtk::ExpressionWatch>;
|
||||
|
||||
/// Unwatch and drop all the expression watches on this widget.
|
||||
fn clear_expression_watches(&self);
|
||||
|
||||
/// Set the actions available on `self` for `event`.
|
||||
///
|
||||
/// Unsets the actions if `event` is `None`.
|
||||
///
|
||||
/// Should be paired with the `EventActions` menu models.
|
||||
fn set_event_actions(&self, event: Option<&Event>) -> Option<gio::SimpleActionGroup> {
|
||||
self.clear_expression_watches();
|
||||
let event = match event {
|
||||
Some(event) => event,
|
||||
None => {
|
||||
self.insert_action_group("event", gio::ActionGroup::NONE);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let action_group = gio::SimpleActionGroup::new();
|
||||
|
||||
if event.raw().is_some() {
|
||||
action_group.add_action_entries([
|
||||
// View Event Source
|
||||
gio::ActionEntry::builder("view-source")
|
||||
.activate(clone!(@weak self as widget, @weak event => move |_, _, _| {
|
||||
let window = widget.root().and_downcast().unwrap();
|
||||
let dialog = EventSourceDialog::new(&window, &event);
|
||||
dialog.present();
|
||||
}))
|
||||
.build(),
|
||||
]);
|
||||
}
|
||||
|
||||
if event.event_id().is_some() {
|
||||
action_group.add_action_entries([
|
||||
// Create a permalink
|
||||
gio::ActionEntry::builder("permalink")
|
||||
.activate(clone!(@weak self as widget, @weak event => move |_, _, _| {
|
||||
let matrix_room = event.room().matrix_room();
|
||||
let event_id = event.event_id().unwrap();
|
||||
spawn!(clone!(@weak widget => async move {
|
||||
let handle = spawn_tokio!(async move {
|
||||
matrix_room.matrix_to_event_permalink(event_id).await
|
||||
});
|
||||
match handle.await.unwrap() {
|
||||
Ok(permalink) => {
|
||||
widget.clipboard().set_text(&permalink.to_string());
|
||||
toast!(widget, gettext("Permalink copied to clipboard"));
|
||||
},
|
||||
Err(error) => {
|
||||
error!("Could not get permalink: {error}");
|
||||
toast!(widget, gettext("Failed to copy the permalink"));
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}))
|
||||
.build()
|
||||
]);
|
||||
|
||||
if let TimelineItemContent::Message(message) = event.content() {
|
||||
let own_user_id = event
|
||||
.room()
|
||||
.session()
|
||||
.user()
|
||||
.map(|user| user.user_id())
|
||||
.unwrap();
|
||||
let is_from_own_user = event.sender_id() == own_user_id;
|
||||
|
||||
// Remove message
|
||||
fn update_remove_action(
|
||||
action_group: &gio::SimpleActionGroup,
|
||||
event: &Event,
|
||||
allowed: bool,
|
||||
) {
|
||||
if allowed {
|
||||
action_group.add_action_entries([gio::ActionEntry::builder("remove")
|
||||
.activate(clone!(@weak event, => move |_, _, _| {
|
||||
if let Some(event_id) = event.event_id() {
|
||||
event.room().redact(event_id, None);
|
||||
}
|
||||
}))
|
||||
.build()]);
|
||||
} else {
|
||||
action_group.remove_action("remove");
|
||||
}
|
||||
}
|
||||
|
||||
if is_from_own_user {
|
||||
update_remove_action(&action_group, event, true);
|
||||
} else {
|
||||
let remove_watch = event
|
||||
.room()
|
||||
.own_user_is_allowed_to_expr(PowerLevelAction::Redact)
|
||||
.watch(
|
||||
glib::Object::NONE,
|
||||
clone!(@weak self as widget, @weak action_group, @weak event => move || {
|
||||
let Some(allowed) = widget.expression_watch(&"remove").and_then(|e| e.evaluate_as::<bool>()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
update_remove_action(&action_group, &event, allowed);
|
||||
}),
|
||||
);
|
||||
|
||||
let allowed = remove_watch.evaluate_as::<bool>().unwrap();
|
||||
update_remove_action(&action_group, event, allowed);
|
||||
|
||||
self.set_expression_watch("remove", remove_watch);
|
||||
}
|
||||
|
||||
action_group.add_action_entries([
|
||||
// Send/redact a reaction
|
||||
gio::ActionEntry::builder("toggle-reaction")
|
||||
.parameter_type(Some(&String::static_variant_type()))
|
||||
.activate(clone!(@weak event => move |_, _, variant| {
|
||||
let key: String = variant.unwrap().get().unwrap();
|
||||
let room = event.room();
|
||||
|
||||
let reaction_group = event.reactions().reaction_group_by_key(&key);
|
||||
|
||||
if let Some(reaction_key) = reaction_group.and_then(|group| group.user_reaction_event_key()) {
|
||||
// The user already sent that reaction, redact it if it has been sent.
|
||||
if let EventKey::EventId(reaction_id) = reaction_key {
|
||||
room.redact(reaction_id, None);
|
||||
}
|
||||
} else if let Some(event_id) = event.event_id() {
|
||||
// The user didn't send that reaction, send it.
|
||||
room.send_reaction(key, event_id);
|
||||
}
|
||||
}))
|
||||
.build(),
|
||||
// Reply
|
||||
gio::ActionEntry::builder("reply")
|
||||
.activate(clone!(@weak event, @weak self as widget => move |_, _, _| {
|
||||
if let Some(event_id) = event.event_id() {
|
||||
let _ = widget.activate_action(
|
||||
"room-history.reply",
|
||||
Some(&event_id.as_str().to_variant())
|
||||
);
|
||||
}
|
||||
}))
|
||||
.build()
|
||||
]);
|
||||
|
||||
match message.msgtype() {
|
||||
MessageType::Text(text_message) => {
|
||||
// Copy text message.
|
||||
let body = text_message.body.clone();
|
||||
|
||||
action_group.add_action_entries([gio::ActionEntry::builder("copy-text")
|
||||
.activate(clone!(@weak self as widget => move |_, _, _| {
|
||||
widget.clipboard().set_text(&body);
|
||||
toast!(widget, gettext("Message copied to clipboard"));
|
||||
}))
|
||||
.build()]);
|
||||
|
||||
// Edit
|
||||
if is_from_own_user {
|
||||
action_group.add_action_entries([gio::ActionEntry::builder("edit")
|
||||
.activate(
|
||||
clone!(@weak event, @weak self as widget => move |_, _, _| {
|
||||
if let Some(event_id) = event.event_id() {
|
||||
let _ = widget.activate_action(
|
||||
"room-history.edit",
|
||||
Some(&event_id.as_str().to_variant())
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.build()]);
|
||||
}
|
||||
}
|
||||
MessageType::File(_) => {
|
||||
// Save message's file.
|
||||
action_group.add_action_entries([gio::ActionEntry::builder("file-save")
|
||||
.activate(clone!(@weak self as widget, @weak event => move |_, _, _| {
|
||||
widget.save_event_file(event);
|
||||
}))
|
||||
.build()]);
|
||||
}
|
||||
MessageType::Emote(message) => {
|
||||
// Copy text message.
|
||||
let message = message.clone();
|
||||
|
||||
action_group.add_action_entries([gio::ActionEntry::builder("copy-text")
|
||||
.activate(clone!(@weak self as widget, @weak event => move |_, _, _| {
|
||||
let display_name = event.sender().display_name();
|
||||
let message = format!("{display_name} {}", message.body);
|
||||
widget.clipboard().set_text(&message);
|
||||
toast!(widget, gettext("Message copied to clipboard"));
|
||||
}))
|
||||
.build()]);
|
||||
|
||||
// Edit
|
||||
if is_from_own_user {
|
||||
action_group.add_action_entries([gio::ActionEntry::builder("edit")
|
||||
.activate(
|
||||
clone!(@weak event, @weak self as widget => move |_, _, _| {
|
||||
if let Some(event_id) = event.event_id() {
|
||||
let _ = widget.activate_action(
|
||||
"room-history.edit",
|
||||
Some(&event_id.as_str().to_variant())
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.build()]);
|
||||
}
|
||||
}
|
||||
MessageType::Notice(message) => {
|
||||
// Copy text message.
|
||||
let body = message.body.clone();
|
||||
|
||||
action_group.add_action_entries([gio::ActionEntry::builder("copy-text")
|
||||
.activate(clone!(@weak self as widget => move |_, _, _| {
|
||||
widget.clipboard().set_text(&body);
|
||||
toast!(widget, gettext("Message copied to clipboard"));
|
||||
}))
|
||||
.build()]);
|
||||
}
|
||||
MessageType::Image(_) => {
|
||||
action_group.add_action_entries([
|
||||
// Copy the texture to the clipboard.
|
||||
gio::ActionEntry::builder("copy-image")
|
||||
.activate(clone!(@weak self as widget, @weak event => move |_, _, _| {
|
||||
let texture = widget.texture().expect("A widget with an image should have a texture");
|
||||
|
||||
widget.clipboard().set_texture(&texture);
|
||||
toast!(widget, gettext("Thumbnail copied to clipboard"));
|
||||
})
|
||||
).build(),
|
||||
// Save the image to a file.
|
||||
gio::ActionEntry::builder("save-image")
|
||||
.activate(clone!(@weak self as widget, @weak event => move |_, _, _| {
|
||||
widget.save_event_file(event);
|
||||
})
|
||||
).build()
|
||||
]);
|
||||
}
|
||||
MessageType::Video(_) => {
|
||||
// Save the video to a file.
|
||||
action_group.add_action_entries([gio::ActionEntry::builder("save-video")
|
||||
.activate(clone!(@weak self as widget, @weak event => move |_, _, _| {
|
||||
widget.save_event_file(event);
|
||||
}))
|
||||
.build()]);
|
||||
}
|
||||
MessageType::Audio(_) => {
|
||||
// Save the audio to a file.
|
||||
action_group.add_action_entries([gio::ActionEntry::builder("save-audio")
|
||||
.activate(clone!(@weak self as widget, @weak event => move |_, _, _| {
|
||||
widget.save_event_file(event);
|
||||
}))
|
||||
.build()]);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.insert_action_group("event", Some(&action_group));
|
||||
|
||||
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) {
|
||||
spawn!(clone!(@weak self as obj => async move {
|
||||
let (filename, data) = match event.get_media_content().await {
|
||||
Ok(res) => res,
|
||||
Err(error) => {
|
||||
error!("Could not get event file: {error}");
|
||||
toast!(obj, error.to_user_facing());
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
save_to_file(&obj, data, filename).await;
|
||||
}));
|
||||
}
|
||||
|
||||
/// Get the texture displayed by this widget, if any.
|
||||
fn texture(&self) -> Option<gdk::Texture>;
|
||||
}
|
|
@ -1,13 +1,21 @@
|
|||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gettextrs::gettext;
|
||||
use gtk::{gdk, gio, glib, glib::clone};
|
||||
use gtk::{gio, glib, glib::clone};
|
||||
use matrix_sdk_ui::timeline::TimelineItemContent;
|
||||
use once_cell::sync::Lazy;
|
||||
use ruma::events::room::{message::MessageType, power_levels::PowerLevelAction};
|
||||
use tracing::error;
|
||||
|
||||
use super::{DividerRow, EventActions, MessageRow, RoomHistory, StateRow, TypingRow};
|
||||
use super::{DividerRow, MessageRow, RoomHistory, StateRow, TypingRow};
|
||||
use crate::{
|
||||
components::{ContextMenuBin, ContextMenuBinExt, ContextMenuBinImpl, ReactionChooser, Spinner},
|
||||
session::model::{Event, TimelineItem, VirtualItem, VirtualItemKind},
|
||||
utils::BoundObjectWeakRef,
|
||||
prelude::*,
|
||||
session::{
|
||||
model::{Event, EventKey, TimelineItem, VirtualItem, VirtualItemKind},
|
||||
view::EventSourceDialog,
|
||||
},
|
||||
spawn, spawn_tokio, toast,
|
||||
utils::{media::save_to_file, BoundObjectWeakRef},
|
||||
};
|
||||
|
||||
mod imp {
|
||||
|
@ -42,7 +50,6 @@ mod imp {
|
|||
|
||||
impl ObjectImpl for ItemRow {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
use once_cell::sync::Lazy;
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
glib::ParamSpecObject::builder::<TimelineItem>("item").build(),
|
||||
|
@ -139,7 +146,7 @@ mod imp {
|
|||
.downcast_ref::<Event>()
|
||||
.filter(|event| event.is_message())
|
||||
{
|
||||
let menu_model = Self::Type::event_message_menu_model();
|
||||
let menu_model = event_message_menu_model();
|
||||
let reaction_chooser = room_history.item_reaction_chooser();
|
||||
if popover.menu_model().as_ref() != Some(menu_model) {
|
||||
popover.set_menu_model(Some(menu_model));
|
||||
|
@ -155,7 +162,7 @@ mod imp {
|
|||
}));
|
||||
obj.action_group().unwrap().add_action(&more_reactions);
|
||||
} else {
|
||||
let menu_model = Self::Type::event_state_menu_model();
|
||||
let menu_model = event_state_menu_model();
|
||||
if popover.menu_model().as_ref() != Some(menu_model) {
|
||||
popover.set_menu_model(Some(menu_model));
|
||||
}
|
||||
|
@ -405,13 +412,288 @@ impl ItemRow {
|
|||
self.remove_css_class("selected");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventActions for ItemRow {
|
||||
fn texture(&self) -> Option<gdk::Texture> {
|
||||
self.child()
|
||||
.and_downcast::<MessageRow>()
|
||||
.and_then(|r| r.texture())
|
||||
/// Set the actions available on `self` for `event`.
|
||||
///
|
||||
/// Unsets the actions if `event` is `None`.
|
||||
fn set_event_actions(&self, event: Option<&Event>) -> Option<gio::SimpleActionGroup> {
|
||||
self.clear_expression_watches();
|
||||
let event = match event {
|
||||
Some(event) => event,
|
||||
None => {
|
||||
self.insert_action_group("event", gio::ActionGroup::NONE);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let action_group = gio::SimpleActionGroup::new();
|
||||
|
||||
if event.raw().is_some() {
|
||||
action_group.add_action_entries([
|
||||
// View Event Source
|
||||
gio::ActionEntry::builder("view-source")
|
||||
.activate(clone!(@weak self as widget, @weak event => move |_, _, _| {
|
||||
let window = widget.root().and_downcast().unwrap();
|
||||
let dialog = EventSourceDialog::new(&window, &event);
|
||||
dialog.present();
|
||||
}))
|
||||
.build(),
|
||||
]);
|
||||
}
|
||||
|
||||
if event.event_id().is_some() {
|
||||
action_group.add_action_entries([
|
||||
// Create a permalink
|
||||
gio::ActionEntry::builder("permalink")
|
||||
.activate(clone!(@weak self as widget, @weak event => move |_, _, _| {
|
||||
let matrix_room = event.room().matrix_room();
|
||||
let event_id = event.event_id().unwrap();
|
||||
spawn!(clone!(@weak widget => async move {
|
||||
let handle = spawn_tokio!(async move {
|
||||
matrix_room.matrix_to_event_permalink(event_id).await
|
||||
});
|
||||
match handle.await.unwrap() {
|
||||
Ok(permalink) => {
|
||||
widget.clipboard().set_text(&permalink.to_string());
|
||||
toast!(widget, gettext("Permalink copied to clipboard"));
|
||||
},
|
||||
Err(error) => {
|
||||
error!("Could not get permalink: {error}");
|
||||
toast!(widget, gettext("Failed to copy the permalink"));
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}))
|
||||
.build()
|
||||
]);
|
||||
|
||||
if let TimelineItemContent::Message(message) = event.content() {
|
||||
let own_user_id = event
|
||||
.room()
|
||||
.session()
|
||||
.user()
|
||||
.map(|user| user.user_id())
|
||||
.unwrap();
|
||||
let is_from_own_user = event.sender_id() == own_user_id;
|
||||
|
||||
// Remove message
|
||||
fn update_remove_action(
|
||||
action_group: &gio::SimpleActionGroup,
|
||||
event: &Event,
|
||||
allowed: bool,
|
||||
) {
|
||||
if allowed {
|
||||
action_group.add_action_entries([gio::ActionEntry::builder("remove")
|
||||
.activate(clone!(@weak event, => move |_, _, _| {
|
||||
if let Some(event_id) = event.event_id() {
|
||||
event.room().redact(event_id, None);
|
||||
}
|
||||
}))
|
||||
.build()]);
|
||||
} else {
|
||||
action_group.remove_action("remove");
|
||||
}
|
||||
}
|
||||
|
||||
if is_from_own_user {
|
||||
update_remove_action(&action_group, event, true);
|
||||
} else {
|
||||
let remove_watch = event
|
||||
.room()
|
||||
.own_user_is_allowed_to_expr(PowerLevelAction::Redact)
|
||||
.watch(
|
||||
glib::Object::NONE,
|
||||
clone!(@weak self as widget, @weak action_group, @weak event => move || {
|
||||
let Some(allowed) = widget.expression_watch(&"remove").and_then(|e| e.evaluate_as::<bool>()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
update_remove_action(&action_group, &event, allowed);
|
||||
}),
|
||||
);
|
||||
|
||||
let allowed = remove_watch.evaluate_as::<bool>().unwrap();
|
||||
update_remove_action(&action_group, event, allowed);
|
||||
|
||||
self.set_expression_watch("remove", remove_watch);
|
||||
}
|
||||
|
||||
action_group.add_action_entries([
|
||||
// Send/redact a reaction
|
||||
gio::ActionEntry::builder("toggle-reaction")
|
||||
.parameter_type(Some(&String::static_variant_type()))
|
||||
.activate(clone!(@weak event => move |_, _, variant| {
|
||||
let key: String = variant.unwrap().get().unwrap();
|
||||
let room = event.room();
|
||||
|
||||
let reaction_group = event.reactions().reaction_group_by_key(&key);
|
||||
|
||||
if let Some(reaction_key) = reaction_group.and_then(|group| group.user_reaction_event_key()) {
|
||||
// The user already sent that reaction, redact it if it has been sent.
|
||||
if let EventKey::EventId(reaction_id) = reaction_key {
|
||||
room.redact(reaction_id, None);
|
||||
}
|
||||
} else if let Some(event_id) = event.event_id() {
|
||||
// The user didn't send that reaction, send it.
|
||||
room.send_reaction(key, event_id);
|
||||
}
|
||||
}))
|
||||
.build(),
|
||||
// Reply
|
||||
gio::ActionEntry::builder("reply")
|
||||
.activate(clone!(@weak event, @weak self as widget => move |_, _, _| {
|
||||
if let Some(event_id) = event.event_id() {
|
||||
let _ = widget.activate_action(
|
||||
"room-history.reply",
|
||||
Some(&event_id.as_str().to_variant())
|
||||
);
|
||||
}
|
||||
}))
|
||||
.build()
|
||||
]);
|
||||
|
||||
match message.msgtype() {
|
||||
MessageType::Text(text_message) => {
|
||||
// Copy text message.
|
||||
let body = text_message.body.clone();
|
||||
|
||||
action_group.add_action_entries([gio::ActionEntry::builder("copy-text")
|
||||
.activate(clone!(@weak self as widget => move |_, _, _| {
|
||||
widget.clipboard().set_text(&body);
|
||||
toast!(widget, gettext("Message copied to clipboard"));
|
||||
}))
|
||||
.build()]);
|
||||
|
||||
// Edit
|
||||
if is_from_own_user {
|
||||
action_group.add_action_entries([gio::ActionEntry::builder("edit")
|
||||
.activate(
|
||||
clone!(@weak event, @weak self as widget => move |_, _, _| {
|
||||
if let Some(event_id) = event.event_id() {
|
||||
let _ = widget.activate_action(
|
||||
"room-history.edit",
|
||||
Some(&event_id.as_str().to_variant())
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.build()]);
|
||||
}
|
||||
}
|
||||
MessageType::File(_) => {
|
||||
// Save message's file.
|
||||
action_group.add_action_entries([gio::ActionEntry::builder("file-save")
|
||||
.activate(clone!(@weak self as widget, @weak event => move |_, _, _| {
|
||||
widget.save_event_file(event);
|
||||
}))
|
||||
.build()]);
|
||||
}
|
||||
MessageType::Emote(message) => {
|
||||
// Copy text message.
|
||||
let message = message.clone();
|
||||
|
||||
action_group.add_action_entries([gio::ActionEntry::builder("copy-text")
|
||||
.activate(clone!(@weak self as widget, @weak event => move |_, _, _| {
|
||||
let display_name = event.sender().display_name();
|
||||
let message = format!("{display_name} {}", message.body);
|
||||
widget.clipboard().set_text(&message);
|
||||
toast!(widget, gettext("Message copied to clipboard"));
|
||||
}))
|
||||
.build()]);
|
||||
|
||||
// Edit
|
||||
if is_from_own_user {
|
||||
action_group.add_action_entries([gio::ActionEntry::builder("edit")
|
||||
.activate(
|
||||
clone!(@weak event, @weak self as widget => move |_, _, _| {
|
||||
if let Some(event_id) = event.event_id() {
|
||||
let _ = widget.activate_action(
|
||||
"room-history.edit",
|
||||
Some(&event_id.as_str().to_variant())
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.build()]);
|
||||
}
|
||||
}
|
||||
MessageType::Notice(message) => {
|
||||
// Copy text message.
|
||||
let body = message.body.clone();
|
||||
|
||||
action_group.add_action_entries([gio::ActionEntry::builder("copy-text")
|
||||
.activate(clone!(@weak self as widget => move |_, _, _| {
|
||||
widget.clipboard().set_text(&body);
|
||||
toast!(widget, gettext("Message copied to clipboard"));
|
||||
}))
|
||||
.build()]);
|
||||
}
|
||||
MessageType::Image(_) => {
|
||||
action_group.add_action_entries([
|
||||
// Copy the texture to the clipboard.
|
||||
gio::ActionEntry::builder("copy-image")
|
||||
.activate(clone!(@weak self as widget, @weak event => move |_, _, _| {
|
||||
let texture = widget.child()
|
||||
.and_downcast::<MessageRow>()
|
||||
.and_then(|r| r.texture())
|
||||
.expect("An ItemRow with an image should have a texture");
|
||||
|
||||
widget.clipboard().set_texture(&texture);
|
||||
toast!(widget, gettext("Thumbnail copied to clipboard"));
|
||||
})
|
||||
).build(),
|
||||
// Save the image to a file.
|
||||
gio::ActionEntry::builder("save-image")
|
||||
.activate(clone!(@weak self as widget, @weak event => move |_, _, _| {
|
||||
widget.save_event_file(event);
|
||||
})
|
||||
).build()
|
||||
]);
|
||||
}
|
||||
MessageType::Video(_) => {
|
||||
// Save the video to a file.
|
||||
action_group.add_action_entries([gio::ActionEntry::builder("save-video")
|
||||
.activate(clone!(@weak self as widget, @weak event => move |_, _, _| {
|
||||
widget.save_event_file(event);
|
||||
}))
|
||||
.build()]);
|
||||
}
|
||||
MessageType::Audio(_) => {
|
||||
// Save the audio to a file.
|
||||
action_group.add_action_entries([gio::ActionEntry::builder("save-audio")
|
||||
.activate(clone!(@weak self as widget, @weak event => move |_, _, _| {
|
||||
widget.save_event_file(event);
|
||||
}))
|
||||
.build()]);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.insert_action_group("event", Some(&action_group));
|
||||
|
||||
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) {
|
||||
spawn!(clone!(@weak self as obj => async move {
|
||||
let (filename, data) = match event.get_media_content().await {
|
||||
Ok(res) => res,
|
||||
Err(error) => {
|
||||
error!("Could not get event file: {error}");
|
||||
toast!(obj, error.to_user_facing());
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
save_to_file(&obj, data, filename).await;
|
||||
}));
|
||||
}
|
||||
|
||||
fn set_expression_watch(&self, key: &'static str, expr_watch: gtk::ExpressionWatch) {
|
||||
|
@ -435,3 +717,38 @@ impl EventActions for ItemRow {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is only safe because the trait `EventActions` can
|
||||
// only be implemented on `gtk::Widgets` that run only on the main thread
|
||||
struct MenuModelSendSync(gio::MenuModel);
|
||||
#[allow(clippy::non_send_fields_in_send_ty)]
|
||||
unsafe impl Send for MenuModelSendSync {}
|
||||
unsafe impl Sync for MenuModelSendSync {}
|
||||
|
||||
/// The `MenuModel` for common message event actions.
|
||||
fn event_message_menu_model() -> &'static gio::MenuModel {
|
||||
static MODEL: Lazy<MenuModelSendSync> = Lazy::new(|| {
|
||||
MenuModelSendSync(
|
||||
gtk::Builder::from_resource(
|
||||
"/org/gnome/Fractal/ui/session/view/content/room_history/event_actions.ui",
|
||||
)
|
||||
.object::<gio::MenuModel>("message_menu_model")
|
||||
.unwrap(),
|
||||
)
|
||||
});
|
||||
&MODEL.0
|
||||
}
|
||||
|
||||
/// The `MenuModel` for common state event actions.
|
||||
fn event_state_menu_model() -> &'static gio::MenuModel {
|
||||
static MODEL: Lazy<MenuModelSendSync> = Lazy::new(|| {
|
||||
MenuModelSendSync(
|
||||
gtk::Builder::from_resource(
|
||||
"/org/gnome/Fractal/ui/session/view/content/room_history/event_actions.ui",
|
||||
)
|
||||
.object::<gio::MenuModel>("state_menu_model")
|
||||
.unwrap(),
|
||||
)
|
||||
});
|
||||
&MODEL.0
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
mod attachment_dialog;
|
||||
mod completion;
|
||||
mod divider_row;
|
||||
mod event_actions;
|
||||
mod item_row;
|
||||
mod message_row;
|
||||
mod read_receipts_list;
|
||||
|
@ -57,7 +56,6 @@ use self::{
|
|||
attachment_dialog::AttachmentDialog,
|
||||
completion::CompletionPopover,
|
||||
divider_row::DividerRow,
|
||||
event_actions::EventActions,
|
||||
item_row::ItemRow,
|
||||
message_row::{content::MessageContent, MessageRow},
|
||||
read_receipts_list::ReadReceiptsList,
|
||||
|
|
Loading…
Reference in a new issue