Browse Source

room-history: Remove sender avatar context menu

The avatar now opens the sender profile, where all the same actions can
be performed.
fractal-13
Kévin Commaille 7 months ago
parent
commit
31c0607568
No known key found for this signature in database
GPG Key ID: F26F4BE20A08255B
  1. 43
      data/resources/stylesheet/_room_history.scss
  2. 3
      po/POTFILES.in
  3. 38
      src/session/view/content/room_history/message_row/mod.rs
  4. 21
      src/session/view/content/room_history/message_row/mod.ui
  5. 25
      src/session/view/content/room_history/mod.rs
  6. 87
      src/session/view/content/room_history/mod.ui
  7. 817
      src/session/view/content/room_history/sender_avatar/mod.rs
  8. 46
      src/session/view/content/room_history/sender_avatar/mod.ui
  9. 1
      src/ui-resources.gresource.xml

43
data/resources/stylesheet/_room_history.scss

@ -99,43 +99,30 @@ room-title {
}
}
}
}
sender-avatar {
padding: 5px;
border-radius: 100%;
@include vendor.focus-ring();
button.sender-avatar {
padding: 5px;
&:hover {
background-color: vendor.$hover_color;
/* @include vendor.focus-ring(); */
image {
filter: brightness(1.07) ;
&:hover {
image {
filter: brightness(1.07) ;
}
}
}
&:active {
background-color: vendor.$active_color;
image {
filter: brightness(1.16) ;
&:active {
image {
filter: brightness(1.16) ;
}
}
}
&:checked {
background-color: vendor.$selected_color;
image {
filter: brightness(1.1) ;
&:checked {
image {
filter: brightness(1.1) ;
}
}
}
popover button.text-button {
padding-left: 10px;
padding-right: 10px;
font-weight: 400;
}
}
message-sender {

3
po/POTFILES.in

@ -162,6 +162,7 @@ src/session/view/content/room_history/message_row/location.rs
src/session/view/content/room_history/message_row/location.ui
src/session/view/content/room_history/message_row/message_state_stack.ui
src/session/view/content/room_history/message_row/mod.rs
src/session/view/content/room_history/message_row/mod.ui
src/session/view/content/room_history/message_row/reaction/mod.rs
src/session/view/content/room_history/message_row/reaction_list.ui
src/session/view/content/room_history/message_row/reply.ui
@ -177,8 +178,6 @@ src/session/view/content/room_history/member_timestamp/row.rs
src/session/view/content/room_history/mod.rs
src/session/view/content/room_history/mod.ui
src/session/view/content/room_history/read_receipts_list/mod.rs
src/session/view/content/room_history/sender_avatar/mod.rs
src/session/view/content/room_history/sender_avatar/mod.ui
src/session/view/content/room_history/state/content.rs
src/session/view/content/room_history/state/creation.rs
src/session/view/content/room_history/state/creation.ui

38
src/session/view/content/room_history/message_row/mod.rs

@ -1,5 +1,6 @@
use adw::{prelude::*, subclass::prelude::*};
use gtk::{CompositeTemplate, gdk, glib, glib::clone};
use tracing::error;
mod audio;
mod caption;
@ -19,11 +20,13 @@ use self::{
message_state_stack::MessageStateStack, reaction_list::MessageReactionList,
sender_name::MessageSenderName,
};
use super::{ReadReceiptsList, SenderAvatar};
use super::ReadReceiptsList;
use crate::{
Application, gettext_f,
Application,
components::UserProfileDialog,
gettext_f,
prelude::*,
session::model::{Event, EventHeaderState},
session::model::{Event, EventHeaderState, Member},
system_settings::ClockFormat,
utils::BoundObject,
};
@ -42,7 +45,7 @@ mod imp {
#[properties(wrapper_type = super::MessageRow)]
pub struct MessageRow {
#[template_child]
avatar: TemplateChild<SenderAvatar>,
avatar_button: TemplateChild<gtk::Button>,
#[template_child]
header: TemplateChild<gtk::Box>,
#[template_child]
@ -62,6 +65,9 @@ mod imp {
/// The event that is presented.
#[property(get, set = Self::set_event, explicit_notify)]
event: BoundObject<Event>,
/// The sender of the event that is presented.
#[property(get = Self::sender)]
sender: PhantomData<Option<Member>>,
/// The texture of the image preview displayed by the descendant of this
/// widget, if any.
#[property(get = Self::texture)]
@ -76,6 +82,7 @@ mod imp {
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::bind_template_callbacks(klass);
}
fn instance_init(obj: &InitializingObject<Self>) {
@ -133,6 +140,7 @@ mod imp {
impl WidgetImpl for MessageRow {}
impl BinImpl for MessageRow {}
#[gtk::template_callbacks]
impl MessageRow {
/// Set the event that is presented.
fn set_event(&self, event: Event) {
@ -145,7 +153,6 @@ mod imp {
}
let sender = event.sender();
self.avatar.set_sender(Some(sender.clone()));
self.display_name.set_sender(Some(sender));
let state_binding = event
@ -193,12 +200,18 @@ mod imp {
],
);
obj.notify_event();
obj.notify_sender();
self.update_content();
self.update_header();
self.update_timestamp();
}
/// The sender of the event that is presented.
fn sender(&self) -> Option<Member> {
self.event.obj().map(|event| event.sender())
}
/// Update the header for the current event.
fn update_header(&self) {
let Some(event) = self.event.obj() else {
@ -209,7 +222,7 @@ mod imp {
let avatar_name_visible = header_state == EventHeaderState::Full;
let header_visible = header_state != EventHeaderState::Hidden;
self.avatar.set_visible(avatar_name_visible);
self.avatar_button.set_visible(avatar_name_visible);
self.display_name.set_visible(avatar_name_visible);
self.header.set_visible(header_visible);
@ -258,6 +271,19 @@ mod imp {
pub(super) fn texture(&self) -> Option<gdk::Texture> {
self.content.texture()
}
/// View the profile of the sender.
#[template_callback]
fn view_sender_profile(&self) {
let Some(sender) = self.sender() else {
error!("Could not open profile for missing sender");
return;
};
let dialog = UserProfileDialog::new();
dialog.set_room_member(sender);
dialog.present(Some(&*self.obj()));
}
}
}

21
src/session/view/content/room_history/message_row/mod.ui

@ -8,7 +8,26 @@
</style>
<property name="column-spacing">8</property>
<child>
<object class="ContentSenderAvatar" id="avatar">
<object class="GtkButton" id="avatar_button">
<property name="tooltip-text" translatable="yes">View Sender Profile</property>
<property name="valign">start</property>
<property name="child">
<object class="Avatar" id="avatar">
<property name="size">36</property>
<property name="accessible-role">presentation</property>
<binding name="data">
<lookup name="avatar-data">
<lookup name="sender">ContentMessageRow</lookup>
</lookup>
</binding>
</object>
</property>
<signal name="clicked" handler="view_sender_profile" swapped="yes"/>
<style>
<class name="circular"/>
<class name="flat"/>
<class name="sender-avatar"/>
</style>
<layout>
<property name="column">0</property>
<property name="row">0</property>

25
src/session/view/content/room_history/mod.rs

@ -19,7 +19,6 @@ mod member_timestamp;
mod message_row;
mod message_toolbar;
mod read_receipts_list;
mod sender_avatar;
mod state;
mod title;
mod typing_row;
@ -32,7 +31,6 @@ use self::{
message_row::MessageRow,
message_toolbar::MessageToolbar,
read_receipts_list::ReadReceiptsList,
sender_avatar::SenderAvatar,
state::{StateGroupRow, StateRow},
title::RoomHistoryTitle,
typing_row::TypingRow,
@ -71,8 +69,6 @@ mod imp {
#[template(resource = "/org/gnome/Fractal/ui/session/view/content/room_history/mod.ui")]
#[properties(wrapper_type = super::RoomHistory)]
pub struct RoomHistory {
#[template_child]
sender_menu_model: TemplateChild<gio::Menu>,
#[template_child]
pub(super) header_bar: TemplateChild<adw::HeaderBar>,
#[template_child]
@ -103,7 +99,6 @@ mod imp {
drag_overlay: TemplateChild<DragOverlay>,
/// The context menu for rows presenting an [`Event`].
event_context_menu: OnceCell<EventActionsContextMenu>,
sender_context_menu: OnceCell<gtk::PopoverMenu>,
/// The timeline currently displayed.
#[property(get, set = Self::set_timeline, explicit_notify, nullable)]
timeline: BoundObject<Timeline>,
@ -1127,21 +1122,6 @@ mod imp {
self.event_context_menu.get_or_init(Default::default)
}
/// The context menu for the sender avatars.
pub(super) fn sender_context_menu(&self) -> &gtk::PopoverMenu {
self.sender_context_menu.get_or_init(|| {
let popover = gtk::PopoverMenu::builder()
.has_arrow(false)
.halign(gtk::Align::Start)
.menu_model(&*self.sender_menu_model)
.build();
popover.update_property(&[gtk::accessible::Property::Label(&gettext(
"Sender Context Menu",
))]);
popover
})
}
/// Opens the room details with the given initial view.
fn open_room_details(&self, initial_view: room_details::InitialView) {
let Some(room) = self.room() else {
@ -1205,11 +1185,6 @@ impl RoomHistory {
fn event_context_menu(&self) -> &EventActionsContextMenu {
self.imp().event_context_menu()
}
/// The context menu for the sender avatars.
fn sender_context_menu(&self) -> &gtk::PopoverMenu {
self.imp().sender_context_menu()
}
}
/// Set the proper child of the given `GtkListItem` for the given

87
src/session/view/content/room_history/mod.ui

@ -31,93 +31,6 @@
</item>
</section>
</menu>
<menu id="sender_menu_model">
<section>
<item>
<attribute name="custom">user-id</attribute>
</item>
</section>
<section>
<item>
<!-- Translators: In this string, 'Mention' is a verb. -->
<attribute name="label" translatable="yes">_Mention</attribute>
<attribute name="action">sender-avatar.mention</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
<item>
<!-- Translators: In this string, 'Open' is a verb. -->
<attribute name="label" translatable="yes">_Open Direct Chat</attribute>
<attribute name="action">sender-avatar.open-direct-chat</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Copy _Link</attribute>
<attribute name="action">sender-avatar.permalink</attribute>
</item>
</section>
<section>
<item>
<!-- Translators: In this string, 'Invite' is a verb. -->
<attribute name="label" translatable="yes">_Invite</attribute>
<attribute name="action">sender-avatar.invite</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Revoke _Invite</attribute>
<attribute name="action">sender-avatar.revoke-invite</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
<item>
<!-- Translators: In this string, 'Mute' is a verb, as in 'Mute room member'. -->
<attribute name="label" translatable="yes">M_ute</attribute>
<attribute name="action">sender-avatar.mute</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Unmute</attribute>
<attribute name="action">sender-avatar.unmute</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
<item>
<!-- Translators: In this string, 'Kick' is a verb. -->
<attribute name="label" translatable="yes">_Kick</attribute>
<attribute name="action">sender-avatar.kick</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
<item>
<!-- Translators: In this string, 'Ban' is a verb. -->
<attribute name="label" translatable="yes">_Ban</attribute>
<attribute name="action">sender-avatar.ban</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Un_ban</attribute>
<attribute name="action">sender-avatar.unban</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Remove Messages</attribute>
<attribute name="action">sender-avatar.remove-messages</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
<item>
<attribute name="label" translatable="yes">I_gnore</attribute>
<attribute name="action">sender-avatar.ignore</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Stop I_gnoring</attribute>
<attribute name="action">sender-avatar.stop-ignoring</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">_View Details</attribute>
<attribute name="action">sender-avatar.view-details</attribute>
</item>
</section>
</menu>
<template class="ContentRoomHistory" parent="AdwBin">
<property name="vexpand">True</property>
<property name="hexpand">True</property>

817
src/session/view/content/room_history/sender_avatar/mod.rs

@ -1,817 +0,0 @@
use std::slice;
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::{gettext, ngettext};
use gtk::{CompositeTemplate, gdk, glib, glib::clone};
use ruma::{
Int, OwnedEventId,
events::room::power_levels::{PowerLevelUserAction, UserPowerLevel},
};
use crate::{
Window,
components::{
Avatar, RoomMemberDestructiveAction, UserProfileDialog, confirm_mute_room_member_dialog,
confirm_room_member_destructive_action_dialog,
},
gettext_f,
prelude::*,
session::{
model::{Member, MemberRole, Membership, User},
view::content::RoomHistory,
},
toast,
utils::{BoundObject, key_bindings},
};
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]
avatar: TemplateChild<Avatar>,
#[template_child]
user_id_btn: TemplateChild<gtk::Button>,
/// Whether this avatar is active.
///
/// This avatar is active when the popover is displayed.
#[property(get)]
active: Cell<bool>,
direct_member_handler: RefCell<Option<glib::SignalHandlerId>>,
permissions_handler: RefCell<Option<glib::SignalHandlerId>>,
/// The displayed member.
#[property(get, set = Self::set_sender, explicit_notify, nullable)]
sender: BoundObject<Member>,
/// The popover of this avatar.
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::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, |obj, _, _| {
if let Some(popover) = obj.imp().popover.obj() {
popover.popdown();
}
let Some(sender) = obj.sender() else {
return;
};
obj.clipboard().set_text(sender.user_id().as_str());
toast!(obj, gettext("Matrix user ID copied to clipboard"));
});
klass.install_action("sender-avatar.mention", None, |obj, _, _| {
obj.imp().mention();
});
klass.install_action_async(
"sender-avatar.open-direct-chat",
None,
|obj, _, _| async move {
obj.imp().open_direct_chat().await;
},
);
klass.install_action("sender-avatar.permalink", None, |obj, _, _| {
let Some(sender) = obj.sender() else {
return;
};
obj.clipboard()
.set_text(&sender.matrix_to_uri().to_string());
toast!(obj, gettext("Link copied to clipboard"));
});
klass.install_action_async("sender-avatar.invite", None, |obj, _, _| async move {
obj.imp().invite().await;
});
klass.install_action_async(
"sender-avatar.revoke-invite",
None,
|obj, _, _| async move {
obj.imp().kick().await;
},
);
klass.install_action_async("sender-avatar.mute", None, |obj, _, _| async move {
obj.imp().toggle_muted().await;
});
klass.install_action_async("sender-avatar.unmute", None, |obj, _, _| async move {
obj.imp().toggle_muted().await;
});
klass.install_action_async("sender-avatar.kick", None, |obj, _, _| async move {
obj.imp().kick().await;
});
klass.install_action_async("sender-avatar.ban", None, |obj, _, _| async move {
obj.imp().ban().await;
});
klass.install_action_async("sender-avatar.unban", None, |obj, _, _| async move {
obj.imp().unban().await;
});
klass.install_action_async(
"sender-avatar.remove-messages",
None,
|obj, _, _| async move {
obj.imp().remove_messages().await;
},
);
klass.install_action_async("sender-avatar.ignore", None, |obj, _, _| async move {
obj.imp().toggle_ignored().await;
});
klass.install_action_async(
"sender-avatar.stop-ignoring",
None,
|obj, _, _| async move {
obj.imp().toggle_ignored().await;
},
);
klass.install_action("sender-avatar.view-details", None, |obj, _, _| {
obj.imp().view_details();
});
klass.install_action("sender-avatar.activate", None, |obj, _, _| {
obj.imp().show_popover(1, 0.0, 0.0);
});
key_bindings::add_activate_bindings(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();
self.set_pressed_state(false);
}
fn dispose(&self) {
self.disconnect_signals();
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> {
// Hide the children in the a11y tree.
None
}
}
#[gtk::template_callbacks]
impl SenderAvatar {
/// Set the displayed member.
fn set_sender(&self, sender: Option<Member>) {
let prev_sender = self.sender.obj();
if prev_sender == sender {
return;
}
self.disconnect_signals();
if let Some(sender) = sender {
let room = sender.room();
let direct_member_handler = room.connect_direct_member_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update_actions();
}
));
self.direct_member_handler
.replace(Some(direct_member_handler));
let permissions_handler = 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_changed(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();
}
/// Disconnect all the signals.
fn disconnect_signals(&self) {
if let Some(sender) = self.sender.obj() {
let room = sender.room();
if let Some(handler) = self.direct_member_handler.take() {
room.disconnect(handler);
}
if let Some(handler) = self.permissions_handler.take() {
room.permissions().disconnect(handler);
}
}
self.sender.disconnect_signals();
}
/// Update the accessible label for the current 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)]);
}
/// Update the actions for the current state.
fn update_actions(&self) {
let Some(sender) = self.sender.obj() else {
return;
};
let obj = self.obj();
let room = sender.room();
let is_direct_chat = room.direct_member().is_some();
let permissions = 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_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.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(),
);
}
/// Set the popover of this avatar.
fn set_popover(&self, popover: Option<gtk::PopoverMenu>) {
let old_popover = self.popover.obj();
if old_popover == popover {
return;
}
// Reset the state.
if let Some(popover) = old_popover {
popover.unparent();
popover.remove_child(&*self.user_id_btn);
}
self.popover.disconnect_signals();
self.set_active(false);
if let Some(popover) = popover {
// We need to remove the popover from the previous button, if any.
if popover.parent().is_some() {
popover.unparent();
}
let parent_handler = popover.connect_parent_notify(clone!(
#[weak(rename_to = imp)]
self,
move |popover| {
if popover.parent().is_none_or(|w| w != *imp.obj()) {
imp.popover.disconnect_signals();
popover.remove_child(&*imp.user_id_btn);
}
}
));
let closed_handler = popover.connect_closed(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.set_active(false);
}
));
popover.add_child(&*self.user_id_btn, "user-id");
popover.set_parent(&*self.obj());
self.popover
.set(popover, vec![parent_handler, closed_handler]);
}
}
/// Set whether this avatar is active.
fn set_active(&self, active: bool) {
if self.active.get() == active {
return;
}
self.active.set(active);
self.obj().notify_active();
self.set_pressed_state(active);
}
/// Set the CSS and a11 states.
fn set_pressed_state(&self, pressed: bool) {
let obj = self.obj();
if pressed {
obj.set_state_flags(gtk::StateFlags::CHECKED, false);
} else {
obj.unset_state_flags(gtk::StateFlags::CHECKED);
}
let tristate = if pressed {
gtk::AccessibleTristate::True
} else {
gtk::AccessibleTristate::False
};
obj.update_state(&[gtk::accessible::State::Pressed(tristate)]);
}
/// The `RoomHistory` that is an ancestor of this avatar.
fn room_history(&self) -> Option<RoomHistory> {
self.obj()
.ancestor(RoomHistory::static_type())
.and_downcast()
}
/// Handle a click on the container.
///
/// Shows a popover with the room member menu.
#[template_callback]
fn show_popover(&self, _n_press: i32, x: f64, y: f64) {
let Some(room_history) = self.room_history() else {
return;
};
self.set_active(true);
let popover = room_history.sender_context_menu();
self.set_popover(Some(popover.clone()));
popover.set_pointing_to(Some(&gdk::Rectangle::new(x as i32, y as i32, 0, 0)));
popover.popup();
}
/// Add a mention of the sender to the message composer.
fn mention(&self) {
let Some(sender) = self.sender.obj() else {
return;
};
let Some(room_history) = self.room_history() else {
return;
};
room_history.message_toolbar().mention_member(&sender);
}
/// View the sender details.
fn view_details(&self) {
let Some(sender) = self.sender.obj() else {
return;
};
let dialog = UserProfileDialog::new();
dialog.set_room_member(sender);
dialog.present(Some(&*self.obj()));
}
/// Open a direct chat with the current sender.
///
/// If one doesn't exist already, it is created.
async fn open_direct_chat(&self) {
let Some(sender) = self.sender.obj().and_upcast::<User>() else {
return;
};
let obj = self.obj();
let room = if let Some(room) = sender.direct_chat() {
room
} else {
toast!(obj, &gettext("Creating a new Direct Chat…"));
if let Ok(room) = sender.get_or_create_direct_chat().await {
room
} else {
toast!(obj, &gettext("Could not create a new Direct Chat"));
return;
}
};
let Some(main_window) = obj.root().and_downcast::<Window>() else {
return;
};
main_window.session_view().select_room(room);
}
/// Invite the sender to the room.
async fn invite(&self) {
let Some(sender) = self.sender.obj() else {
return;
};
let obj = self.obj();
toast!(obj, gettext("Inviting user…"));
let room = sender.room();
let user_id = sender.user_id().clone();
if room.invite(&[user_id]).await.is_err() {
toast!(obj, gettext("Could not invite user"));
}
}
/// Kick the user from the room.
async fn kick(&self) {
let Some(sender) = self.sender.obj() else {
return;
};
let obj = self.obj();
let Some(response) = confirm_room_member_destructive_action_dialog(
&sender,
RoomMemberDestructiveAction::Kick,
&*obj,
)
.await
else {
return;
};
let membership = sender.membership();
let label = match membership {
Membership::Invite => gettext("Revoking invite…"),
_ => gettext("Kicking user…"),
};
toast!(obj, 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"),
_ => gettext("Could not kick user"),
};
toast!(obj, error);
}
}
/// (Un)mute the user in the room.
async fn toggle_muted(&self) {
let Some(sender) = self.sender.obj() else {
return;
};
let UserPowerLevel::Int(old_power_level) = sender.power_level() else {
// We cannot mute someone with an infinite power level.
return;
};
let old_power_level = i64::from(old_power_level);
let obj = self.obj();
let permissions = sender.room().permissions();
// Warn if user is muted but was not before.
let mute_power_level = permissions.mute_power_level();
let mute = old_power_level > mute_power_level;
if mute && !confirm_mute_room_member_dialog(slice::from_ref(&sender), &*obj).await {
return;
}
let user_id = sender.user_id().clone();
let (new_power_level, text) = if mute {
(mute_power_level, gettext("Muting member…"))
} else {
(
permissions.default_power_level(),
gettext("Unmuting member…"),
)
};
toast!(obj, text);
let text = if permissions
.set_user_power_level(user_id, Int::new_saturating(new_power_level))
.await
.is_ok()
{
if mute {
gettext("Member muted")
} else {
gettext("Member unmuted")
}
} else if mute {
gettext("Could not mute member")
} else {
gettext("Could not unmute member")
};
toast!(obj, text);
}
/// Ban the room member.
async fn ban(&self) {
let Some(sender) = self.sender.obj() else {
return;
};
let obj = self.obj();
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()),
&*obj,
)
.await
else {
return;
};
toast!(obj, 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!(obj, gettext("Could not ban user"));
}
if response.remove_events {
self.remove_known_messages_inner(&sender, redactable_events, response.reason)
.await;
}
}
/// Unban the room member.
async fn unban(&self) {
let Some(sender) = self.sender.obj() else {
return;
};
let obj = self.obj();
toast!(obj, gettext("Unbanning user…"));
let room = sender.room();
let user_id = sender.user_id().clone();
if room.unban(&[(user_id, None)]).await.is_err() {
toast!(obj, gettext("Could not unban user"));
}
}
/// Remove the known events of the room member.
async fn remove_messages(&self) {
let Some(sender) = self.sender.obj() else {
return;
};
let redactable_events = sender.redactable_events();
let Some(response) = confirm_room_member_destructive_action_dialog(
&sender,
RoomMemberDestructiveAction::RemoveMessages(redactable_events.len()),
&*self.obj(),
)
.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 obj = self.obj();
let n = u32::try_from(events.len()).unwrap_or(u32::MAX);
toast!(
obj,
ngettext(
// Translators: Do NOT translate the content between '{' and '}',
// this is a variable name.
"Removing 1 message sent by the user…",
"Removing {n} messages sent by the user…",
n,
),
n,
);
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!(
obj,
ngettext(
// Translators: Do NOT translate the content between '{' and '}',
// this is a variable name.
"Could not remove 1 message sent by the user",
"Could not remove {n} messages sent by the user",
n,
),
n,
);
}
}
/// Toggle whether the user is ignored or not.
async fn toggle_ignored(&self) {
let Some(sender) = self.sender.obj().and_upcast::<User>() else {
return;
};
let obj = self.obj();
let is_ignored = sender.is_ignored();
let label = if is_ignored {
gettext("Stop ignoring user…")
} else {
gettext("Ignoring user…")
};
toast!(obj, label);
if is_ignored {
if sender.stop_ignoring().await.is_err() {
toast!(obj, gettext("Could not stop ignoring user"));
}
} else if sender.ignore().await.is_err() {
toast!(obj, gettext("Could not ignore user"));
}
}
}
}
glib::wrapper! {
/// An avatar with a popover menu for room members.
pub struct SenderAvatar(ObjectSubclass<imp::SenderAvatar>)
@extends gtk::Widget, @implements gtk::Accessible;
}
impl SenderAvatar {
pub fn new() -> Self {
glib::Object::new()
}
}

46
src/session/view/content/room_history/sender_avatar/mod.ui

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="ContentSenderAvatar" parent="GtkWidget">
<accessibility>
<property name="description" translatable="yes">Open Sender Context Menu</property>
<property name="has-popup">true</property>
</accessibility>
<property name="focusable">true</property>
<property name="valign">start</property>
<child>
<object class="Avatar" id="avatar">
<property name="size">36</property>
<property name="accessible-role">presentation</property>
<binding name="data">
<lookup name="avatar-data">
<lookup name="sender">ContentSenderAvatar</lookup>
</lookup>
</binding>
</object>
</child>
<child>
<object class="GtkGestureClick">
<signal name="released" handler="show_popover" swapped="true"/>
</object>
</child>
</template>
<object class="GtkButton" id="user_id_btn">
<property name="tooltip-text" translatable="yes">Copy Matrix User ID</property>
<property name="action-name">sender-avatar.copy-user-id</property>
<style>
<class name="flat"/>
<class name="text-button" />
</style>
<property name="child">
<object class="GtkLabel">
<binding name="label">
<lookup name="user-id-string">
<lookup name="sender">ContentSenderAvatar</lookup>
</lookup>
</binding>
<property name="ellipsize">end</property>
<property name="xalign">0.0</property>
</object>
</property>
</object>
</interface>

1
src/ui-resources.gresource.xml

@ -130,7 +130,6 @@
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/mod.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/read_receipts_list/mod.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/read_receipts_list/read_receipts_popover.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/sender_avatar/mod.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/state/creation.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/state/group_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/state/row.ui</file>

Loading…
Cancel
Save