use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{gdk, glib, glib::clone, CompositeTemplate};
use ruma::{events::room::power_levels::PowerLevelUserAction, OwnedEventId};
use crate::{
components::{
confirm_mute_room_member_dialog, confirm_room_member_destructive_action_dialog, Avatar,
RoomMemberDestructiveAction, UserProfileDialog,
},
gettext_f, ngettext_f,
prelude::*,
session::{
model::{Member, MemberRole, Membership, User},
view::content::RoomHistory,
},
toast,
utils::{add_activate_binding_action, BoundObject},
Window,
};
mod imp {
use std::cell::{Cell, RefCell};
use glib::subclass::InitializingObject;
use super::*;
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(
resource = "/org/gnome/Fractal/ui/session/view/content/room_history/sender_avatar/mod.ui"
)]
#[properties(wrapper_type = super::SenderAvatar)]
pub struct SenderAvatar {
#[template_child]
pub avatar: TemplateChild<Avatar>,
#[template_child]
pub user_id_btn: TemplateChild<gtk::Button>,
#[property(get)]
pub active: Cell<bool>,
pub permissions_handler: RefCell<Option<glib::SignalHandlerId>>,
#[property(get, set = Self::set_sender, explicit_notify, nullable)]
pub sender: BoundObject<Member>,
pub(super) popover: BoundObject<gtk::PopoverMenu>,
}
#[glib::object_subclass]
impl ObjectSubclass for SenderAvatar {
const NAME: &'static str = "ContentSenderAvatar";
type Type = super::SenderAvatar;
type ParentType = gtk::Widget;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
klass.set_layout_manager_type::<gtk::BinLayout>();
klass.set_css_name("sender-avatar");
klass.set_accessible_role(gtk::AccessibleRole::ToggleButton);
klass.install_action("sender-avatar.copy-user-id", None, |widget, _, _| {
if let Some(popover) = widget.imp().popover.obj() {
popover.popdown();
}
let Some(sender) = widget.sender() else {
return;
};
widget.clipboard().set_text(sender.user_id().as_str());
toast!(widget, gettext("Matrix user ID copied to clipboard"));
});
klass.install_action("sender-avatar.mention", None, |widget, _, _| {
widget.mention();
});
klass.install_action_async(
"sender-avatar.open-direct-chat",
None,
|widget, _, _| async move {
widget.open_direct_chat().await;
},
);
klass.install_action("sender-avatar.permalink", None, |widget, _, _| {
let Some(sender) = widget.sender() else {
return;
};
widget
.clipboard()
.set_text(&sender.matrix_to_uri().to_string());
toast!(widget, gettext("Link copied to clipboard"));
});
klass.install_action_async("sender-avatar.invite", None, |widget, _, _| async move {
widget.invite().await;
});
klass.install_action_async(
"sender-avatar.revoke-invite",
None,
|widget, _, _| async move {
widget.kick().await;
},
);
klass.install_action_async("sender-avatar.mute", None, |obj, _, _| async move {
obj.toggle_muted().await;
});
klass.install_action_async("sender-avatar.unmute", None, |obj, _, _| async move {
obj.toggle_muted().await;
});
klass.install_action_async("sender-avatar.kick", None, |widget, _, _| async move {
widget.kick().await;
});
klass.install_action_async(
"sender-avatar.deny-access",
None,
|widget, _, _| async move {
widget.kick().await;
},
);
klass.install_action_async("sender-avatar.ban", None, |widget, _, _| async move {
widget.ban().await;
});
klass.install_action_async("sender-avatar.unban", None, |widget, _, _| async move {
widget.unban().await;
});
klass.install_action_async(
"sender-avatar.remove-messages",
None,
|widget, _, _| async move {
widget.remove_messages().await;
},
);
klass.install_action_async("sender-avatar.ignore", None, |widget, _, _| async move {
widget.toggle_ignored().await;
});
klass.install_action_async(
"sender-avatar.stop-ignoring",
None,
|widget, _, _| async move {
widget.toggle_ignored().await;
},
);
klass.install_action("sender-avatar.view-details", None, |widget, _, _| {
widget.view_details();
});
klass.install_action("sender-avatar.activate", None, |widget, _, _| {
widget.show_popover(1, 0.0, 0.0);
});
add_activate_binding_action(klass, "sender-avatar.activate");
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for SenderAvatar {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
obj.set_pressed_state(false);
}
fn dispose(&self) {
if let Some(popover) = self.popover.obj() {
popover.unparent();
popover.remove_child(&*self.user_id_btn);
}
self.avatar.unparent();
}
}
impl WidgetImpl for SenderAvatar {}
impl AccessibleImpl for SenderAvatar {
fn first_accessible_child(&self) -> Option<gtk::Accessible> {
None
}
}
impl SenderAvatar {
fn set_sender(&self, sender: Option<Member>) {
let prev_sender = self.sender.obj();
if prev_sender == sender {
return;
}
self.sender.disconnect_signals();
if let Some(sender) = sender {
let permissions_handler = sender.room().permissions().connect_changed(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update_actions();
}
));
self.permissions_handler.replace(Some(permissions_handler));
let display_name_handler = sender.connect_display_name_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update_accessible_label();
}
));
let membership_handler = sender.connect_membership_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update_actions();
}
));
let power_level_handler = sender.connect_power_level_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update_actions();
}
));
let is_ignored_handler = sender.connect_is_ignored_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update_actions();
}
));
self.sender.set(
sender,
vec![
display_name_handler,
membership_handler,
power_level_handler,
is_ignored_handler,
],
);
self.update_accessible_label();
self.update_actions();
}
self.obj().notify_sender();
}
fn update_accessible_label(&self) {
let Some(sender) = self.sender.obj() else {
return;
};
let label = gettext_f("{user}’s avatar", &[("user", &sender.display_name())]);
self.obj()
.update_property(&[gtk::accessible::Property::Label(&label)]);
}
fn update_actions(&self) {
let Some(sender) = self.sender.obj() else {
return;
};
let obj = self.obj();
let permissions = sender.room().permissions();
let membership = sender.membership();
let sender_id = sender.user_id();
let is_own_user = sender.is_own_user();
let power_level = sender.power_level();
let role = permissions.role(power_level);
obj.action_set_enabled(
"sender-avatar.mention",
!is_own_user && membership == Membership::Join && permissions.can_send_message(),
);
obj.action_set_enabled("sender-avatar.open-direct-chat", !is_own_user);
obj.action_set_enabled(
"sender-avatar.invite",
!is_own_user
&& matches!(membership, Membership::Leave | Membership::Knock)
&& permissions.can_do_to_user(sender_id, PowerLevelUserAction::Kick),
);
obj.action_set_enabled(
"sender-avatar.revoke-invite",
!is_own_user
&& membership == Membership::Invite
&& permissions.can_do_to_user(sender_id, PowerLevelUserAction::Kick),
);
obj.action_set_enabled(
"sender-avatar.mute",
!is_own_user
&& role != MemberRole::Muted
&& permissions.default_power_level() > permissions.mute_power_level()
&& permissions
.can_do_to_user(sender_id, PowerLevelUserAction::ChangePowerLevel),
);
obj.action_set_enabled(
"sender-avatar.unmute",
!is_own_user
&& role == MemberRole::Muted
&& permissions.default_power_level() > permissions.mute_power_level()
&& permissions
.can_do_to_user(sender_id, PowerLevelUserAction::ChangePowerLevel),
);
obj.action_set_enabled(
"sender-avatar.kick",
!is_own_user
&& membership == Membership::Join
&& permissions.can_do_to_user(sender_id, PowerLevelUserAction::Kick),
);
obj.action_set_enabled(
"sender-avatar.deny-access",
!is_own_user
&& membership == Membership::Knock
&& permissions.can_do_to_user(sender_id, PowerLevelUserAction::Kick),
);
obj.action_set_enabled(
"sender-avatar.ban",
!is_own_user
&& membership != Membership::Ban
&& permissions.can_do_to_user(sender_id, PowerLevelUserAction::Ban),
);
obj.action_set_enabled(
"sender-avatar.unban",
!is_own_user
&& membership == Membership::Ban
&& permissions.can_do_to_user(sender_id, PowerLevelUserAction::Unban),
);
obj.action_set_enabled(
"sender-avatar.remove-messages",
!is_own_user && permissions.can_redact_other(),
);
obj.action_set_enabled("sender-avatar.ignore", !is_own_user && !sender.is_ignored());
obj.action_set_enabled(
"sender-avatar.stop-ignoring",
!is_own_user && sender.is_ignored(),
);
}
pub(super) fn set_popover(&self, popover: Option<gtk::PopoverMenu>) {
let old_popover = self.popover.obj();
if old_popover == popover {
return;
}
let obj = self.obj();
if let Some(popover) = old_popover {
popover.unparent();
popover.remove_child(&*self.user_id_btn);
}
self.popover.disconnect_signals();
obj.set_active(false);
if let Some(popover) = popover {
if popover.parent().is_some() {
popover.unparent();
}
let parent_handler = popover.connect_parent_notify(clone!(
#[weak]
obj,
move |popover| {
if !popover.parent().is_some_and(|w| w == obj) {
let imp = obj.imp();
imp.popover.disconnect_signals();
popover.remove_child(&*imp.user_id_btn);
}
}
));
let closed_handler = popover.connect_closed(clone!(
#[weak]
obj,
move |_| {
obj.set_active(false);
}
));
popover.add_child(&*self.user_id_btn, "user-id");
popover.set_parent(&*obj);
self.popover
.set(popover, vec![parent_handler, closed_handler]);
}
}
}
}
glib::wrapper! {
pub struct SenderAvatar(ObjectSubclass<imp::SenderAvatar>)
@extends gtk::Widget, @implements gtk::Accessible;
}
#[gtk::template_callbacks]
impl SenderAvatar {
pub fn new() -> Self {
glib::Object::new()
}
fn set_active(&self, active: bool) {
if self.active() == active {
return;
}
self.imp().active.set(active);
self.notify_active();
self.set_pressed_state(active);
}
fn set_pressed_state(&self, pressed: bool) {
if pressed {
self.set_state_flags(gtk::StateFlags::CHECKED, false);
} else {
self.unset_state_flags(gtk::StateFlags::CHECKED);
}
let tristate = if pressed {
gtk::AccessibleTristate::True
} else {
gtk::AccessibleTristate::False
};
self.update_state(&[gtk::accessible::State::Pressed(tristate)]);
}
#[template_callback]
fn show_popover(&self, _n_press: i32, x: f64, y: f64) {
let Some(room_history) = self
.ancestor(RoomHistory::static_type())
.and_downcast::<RoomHistory>()
else {
return;
};
self.set_active(true);
let popover = room_history.sender_context_menu();
self.imp().set_popover(Some(popover.clone()));
popover.set_pointing_to(Some(&gdk::Rectangle::new(x as i32, y as i32, 0, 0)));
popover.popup();
}
fn mention(&self) {
let Some(sender) = self.sender() else {
return;
};
let Some(room_history) = self
.ancestor(RoomHistory::static_type())
.and_downcast::<RoomHistory>()
else {
return;
};
room_history.message_toolbar().mention_member(&sender);
}
fn view_details(&self) {
let Some(sender) = self.sender() else {
return;
};
let dialog = UserProfileDialog::new();
dialog.set_room_member(sender);
dialog.present(Some(self));
}
async fn open_direct_chat(&self) {
let Some(sender) = self.sender().and_upcast::<User>() else {
return;
};
let room = if let Some(room) = sender.direct_chat() {
room
} else {
toast!(self, &gettext("Creating a new Direct Chat…"));
if let Ok(room) = sender.get_or_create_direct_chat().await {
room
} else {
toast!(self, &gettext("Could not create a new Direct Chat"));
return;
}
};
let Some(main_window) = self.root().and_downcast::<Window>() else {
return;
};
main_window.show_room(sender.session().session_id(), room.room_id());
}
async fn invite(&self) {
let Some(sender) = self.sender() else {
return;
};
toast!(self, gettext("Inviting user…"));
let room = sender.room();
let user_id = sender.user_id().clone();
if room.invite(&[user_id]).await.is_err() {
toast!(self, gettext("Could not invite user"));
}
}
async fn kick(&self) {
let Some(sender) = self.sender() else {
return;
};
let Some(response) = confirm_room_member_destructive_action_dialog(
&sender,
RoomMemberDestructiveAction::Kick,
self,
)
.await
else {
return;
};
let membership = sender.membership();
let label = match membership {
Membership::Invite => gettext("Revoking invite…"),
Membership::Knock => gettext("Denying access…"),
_ => gettext("Kicking user…"),
};
toast!(self, label);
let room = sender.room();
let user_id = sender.user_id().clone();
if room.kick(&[(user_id, response.reason)]).await.is_err() {
let error = match membership {
Membership::Invite => gettext("Could not revoke invite of user"),
Membership::Knock => gettext("Could not deny access to user"),
_ => gettext("Could not kick user"),
};
toast!(self, error);
}
}
async fn toggle_muted(&self) {
let Some(sender) = self.sender() else {
return;
};
let old_power_level = sender.power_level();
let permissions = sender.room().permissions();
let mute_power_level = permissions.mute_power_level();
let mute = old_power_level > mute_power_level;
if mute && !confirm_mute_room_member_dialog(&sender, self).await {
return;
}
let user_id = sender.user_id().clone();
let new_power_level = if mute {
toast!(self, gettext("Muting member…"));
mute_power_level
} else {
toast!(self, gettext("Unmuting member…"));
permissions.default_power_level()
};
if permissions
.set_user_power_level(user_id, new_power_level)
.await
.is_ok()
{
if mute {
toast!(self, gettext("Member muted"));
} else {
toast!(self, gettext("Member unmuted"));
}
} else if mute {
toast!(self, gettext("Could not mute member"));
} else {
toast!(self, gettext("Could not unmute member"));
}
}
async fn ban(&self) {
let Some(sender) = self.sender() else {
return;
};
let permissions = sender.room().permissions();
let redactable_events = if permissions.can_redact_other() {
sender.redactable_events()
} else {
vec![]
};
let Some(response) = confirm_room_member_destructive_action_dialog(
&sender,
RoomMemberDestructiveAction::Ban(redactable_events.len()),
self,
)
.await
else {
return;
};
toast!(self, gettext("Banning user…"));
let room = sender.room();
let user_id = sender.user_id().clone();
if room
.ban(&[(user_id, response.reason.clone())])
.await
.is_err()
{
toast!(self, gettext("Could not ban user"));
}
if response.remove_events {
self.remove_known_messages_inner(&sender, redactable_events, response.reason)
.await;
}
}
async fn unban(&self) {
let Some(sender) = self.sender() else {
return;
};
toast!(self, gettext("Unbanning user…"));
let room = sender.room();
let user_id = sender.user_id().clone();
if room.unban(&[(user_id, None)]).await.is_err() {
toast!(self, gettext("Could not unban user"));
}
}
async fn remove_messages(&self) {
let Some(sender) = self.sender() else {
return;
};
let redactable_events = sender.redactable_events();
let Some(response) = confirm_room_member_destructive_action_dialog(
&sender,
RoomMemberDestructiveAction::RemoveMessages(redactable_events.len()),
self,
)
.await
else {
return;
};
self.remove_known_messages_inner(&sender, redactable_events, response.reason)
.await;
}
async fn remove_known_messages_inner(
&self,
sender: &Member,
events: Vec<OwnedEventId>,
reason: Option<String>,
) {
let n = u32::try_from(events.len()).unwrap_or(u32::MAX);
toast!(
self,
ngettext_f(
"Removing 1 message sent by the user…",
"Removing {n} messages sent by the user…",
n,
&[("n", &n.to_string())]
)
);
let room = sender.room();
if let Err(failed_events) = room.redact(&events, reason).await {
let n = u32::try_from(failed_events.len()).unwrap_or(u32::MAX);
toast!(
self,
ngettext_f(
"Could not remove 1 message sent by the user",
"Could not remove {n} messages sent by the user",
n,
&[("n", &n.to_string())]
)
);
}
}
async fn toggle_ignored(&self) {
let Some(sender) = self.sender().and_upcast::<User>() else {
return;
};
let is_ignored = sender.is_ignored();
let label = if is_ignored {
gettext("Stop ignoring user…")
} else {
gettext("Ignoring user…")
};
toast!(self, label);
if is_ignored {
if sender.stop_ignoring().await.is_err() {
toast!(self, gettext("Could not stop ignoring user"));
}
} else if sender.ignore().await.is_err() {
toast!(self, gettext("Could not ignore user"));
}
}
}