Browse Source

room-history: Show a menu when clicking on sender avatar

Offers the same actions as member profile, and mention and permalink
merge-requests/1579/head
Kévin Commaille 2 years ago
parent
commit
0945b18831
No known key found for this signature in database
GPG Key ID: 29A48C1F03620416
  1. 34
      data/resources/style.css
  2. 2
      po/POTFILES.in
  3. 1
      src/prelude.rs
  4. 2
      src/session/view/content/room_details/mod.rs
  5. 13
      src/session/view/content/room_history/item_row.rs
  6. 12
      src/session/view/content/room_history/message_row/mod.rs
  7. 5
      src/session/view/content/room_history/message_row/mod.ui
  8. 19
      src/session/view/content/room_history/message_toolbar/mod.rs
  9. 46
      src/session/view/content/room_history/mod.rs
  10. 76
      src/session/view/content/room_history/mod.ui
  11. 634
      src/session/view/content/room_history/sender_avatar/mod.rs
  12. 38
      src/session/view/content/room_history/sender_avatar/mod.ui
  13. 9
      src/session/view/create_dm_dialog/dm_user.rs
  14. 3
      src/session/view/session_view.rs
  15. 49
      src/session/view/user_profile_dialog.rs
  16. 1
      src/ui-resources.gresource.xml

34
data/resources/style.css

@ -665,6 +665,40 @@ roomtitle .subtitle {
padding-right: 12px;
}
sender-avatar {
padding: 3px;
border-radius: 100%;
transition-property: outline, outline-width, outline-offset, outline-color;
transition-duration: 300ms;
animation-timing-function: ease-in-out;
outline: 0 solid transparent;
outline-offset: 2px;
}
sender-avatar:focus {
outline-color: alpha(@accent_color, 0.5);
outline-width: 2px;
outline-offset: -2px;
}
sender-avatar:hover {
background-color: alpha(currentColor, .07);
}
sender-avatar:active {
background-color: alpha(currentColor, .16);
}
sender-avatar:checked {
background-color: alpha(currentColor, .1);
}
sender-avatar popover button.text-button {
padding-left: 10px;
padding-right: 10px;
font-weight: 400;
}
/* Event Source Dialog */

2
po/POTFILES.in

@ -109,6 +109,8 @@ 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_row/creation.rs
src/session/view/content/room_history/state_row/creation.ui
src/session/view/content/room_history/state_row/mod.rs

1
src/prelude.rs

@ -3,7 +3,6 @@ pub use crate::{
contrib::CameraExt,
session::model::{TimelineItemExt, UserExt},
session_list::SessionInfoExt,
system_settings::SystemSettingsExt,
user_facing_error::UserFacingError,
utils::LocationExt,
};

2
src/session/view/content/room_details/mod.rs

@ -120,7 +120,7 @@ glib::wrapper! {
}
impl RoomDetails {
pub fn new(parent_window: &Option<gtk::Window>, room: &Room) -> Self {
pub fn new(parent_window: Option<&gtk::Window>, room: &Room) -> Self {
glib::Object::builder()
.property("transient-for", parent_window)
.property("room", room)

13
src/session/view/content/room_history/item_row.rs

@ -408,19 +408,22 @@ impl ItemRow {
}
fn show_emoji_chooser(&self, popover: &gtk::PopoverMenu) {
let emoji_chooser = gtk::EmojiChooser::builder().has_arrow(false).build();
let (_, rectangle) = popover.pointing_to();
let emoji_chooser = gtk::EmojiChooser::builder()
.has_arrow(false)
.pointing_to(&rectangle)
.build();
emoji_chooser.connect_emoji_picked(clone!(@weak self as obj => move |_, emoji| {
obj
.activate_action("event.toggle-reaction", Some(&emoji.to_variant()))
.unwrap();
}));
emoji_chooser.set_parent(self);
emoji_chooser.connect_closed(|emoji_chooser| {
emoji_chooser.unparent();
});
let (_, rectangle) = popover.pointing_to();
emoji_chooser.set_pointing_to(Some(&rectangle));
emoji_chooser.set_parent(self);
popover.popdown();
emoji_chooser.popup();

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

@ -18,10 +18,10 @@ pub use self::content::{ContentFormat, MessageContent};
use self::{
media::MessageMedia, message_state_stack::MessageStateStack, reaction_list::MessageReactionList,
};
use super::ReadReceiptsList;
use super::{ReadReceiptsList, SenderAvatar};
use crate::{
components::Avatar, gettext_f, prelude::*, session::model::Event, system_settings::ClockFormat,
utils::BoundObject, Application, Window,
gettext_f, session::model::Event, system_settings::ClockFormat, utils::BoundObject,
Application, Window,
};
mod imp {
@ -38,7 +38,7 @@ mod imp {
#[properties(wrapper_type = super::MessageRow)]
pub struct MessageRow {
#[template_child]
pub avatar: TemplateChild<Avatar>,
pub avatar: TemplateChild<SenderAvatar>,
#[template_child]
pub header: TemplateChild<gtk::Box>,
#[template_child]
@ -157,8 +157,8 @@ mod imp {
binding.unbind();
}
self.avatar
.set_data(Some(event.sender().avatar_data().clone()));
self.avatar.set_room(Some(event.room()));
self.avatar.set_sender(Some(event.sender()));
let display_name_binding = event
.sender()

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

@ -5,10 +5,7 @@
<object class="GtkGrid">
<property name="column-spacing">10</property>
<child>
<object class="ComponentsAvatar" id="avatar">
<property name="size">36</property>
<property name="valign">start</property>
<property name="accessible-role">presentation</property>
<object class="ContentSenderAvatar" id="avatar">
<layout>
<property name="column">0</property>
<property name="row">0</property>

19
src/session/view/content/room_history/message_toolbar/mod.rs

@ -36,7 +36,7 @@ use crate::{
components::{CustomEntry, LabelWithWidgets, Pill},
gettext_f,
prelude::*,
session::model::{Event, EventKey, Room},
session::model::{Event, EventKey, Member, Room},
spawn, toast,
utils::{
matrix::extract_mentions,
@ -330,6 +330,23 @@ impl MessageToolbar {
glib::Object::new()
}
/// Add a mention of the given member to the message composer.
pub fn mention_member(&self, member: &Member) {
let view = &*self.imp().message_entry;
let buffer = view.buffer();
let mut insert = buffer.iter_at_mark(&buffer.get_insert());
let anchor = match insert.child_anchor() {
Some(anchor) => anchor,
None => buffer.create_child_anchor(&mut insert),
};
let pill = member.to_pill();
view.add_child_at_anchor(&pill, &anchor);
view.grab_focus();
}
/// Set the type of related event of the composer.
fn set_related_event_type(&self, related_type: RelatedEventType) {
if self.related_event_type() == related_type {

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

@ -4,6 +4,7 @@ mod member_timestamp;
mod message_row;
mod message_toolbar;
mod read_receipts_list;
mod sender_avatar;
mod state_row;
mod typing_row;
mod verification_info_bar;
@ -26,8 +27,9 @@ use tracing::{error, warn};
use self::{
divider_row::DividerRow, item_row::ItemRow, message_row::MessageRow,
message_toolbar::MessageToolbar, read_receipts_list::ReadReceiptsList, state_row::StateRow,
typing_row::TypingRow, verification_info_bar::VerificationInfoBar,
message_toolbar::MessageToolbar, read_receipts_list::ReadReceiptsList,
sender_avatar::SenderAvatar, state_row::StateRow, typing_row::TypingRow,
verification_info_bar::VerificationInfoBar,
};
use super::{room_details, RoomDetails};
use crate::{
@ -80,6 +82,9 @@ mod imp {
pub sticky: Cell<bool>,
pub item_context_menu: OnceCell<gtk::PopoverMenu>,
pub item_reaction_chooser: ReactionChooser,
pub sender_context_menu: OnceCell<gtk::PopoverMenu>,
#[template_child]
pub sender_menu_model: TemplateChild<gio::Menu>,
#[template_child]
pub room_title: TemplateChild<RoomTitle>,
#[template_child]
@ -609,13 +614,15 @@ impl RoomHistory {
/// If `subpage_name` is set, the room details will be opened on the given
/// subpage.
pub fn open_room_details(&self, subpage_name: Option<room_details::SubpageName>) {
if let Some(room) = self.room() {
let window = RoomDetails::new(&self.parent_window(), &room);
if let Some(subpage_name) = subpage_name {
window.show_initial_subpage(subpage_name);
}
window.present();
let Some(room) = self.room() else {
return;
};
let window = RoomDetails::new(self.root().and_downcast_ref(), &room);
if let Some(subpage_name) = subpage_name {
window.show_initial_subpage(subpage_name);
}
window.present();
}
fn update_room_menu(&self) {
@ -707,11 +714,6 @@ impl RoomHistory {
});
}
/// Returns the parent GtkWindow containing this widget.
fn parent_window(&self) -> Option<gtk::Window> {
self.root().and_downcast()
}
/// Scroll to the newest message in the timeline
pub fn scroll_down(&self) {
let imp = self.imp();
@ -738,6 +740,7 @@ impl RoomHistory {
self.message_toolbar().handle_paste_action();
}
/// The context menu for the item rows.
pub fn item_context_menu(&self) -> &gtk::PopoverMenu {
self.imp().item_context_menu.get_or_init(|| {
let popover = gtk::PopoverMenu::builder()
@ -749,10 +752,27 @@ impl RoomHistory {
})
}
/// The reaction chooser for the item rows.
pub fn item_reaction_chooser(&self) -> &ReactionChooser {
&self.imp().item_reaction_chooser
}
/// The context menu for the sender avatars.
pub fn sender_context_menu(&self) -> &gtk::PopoverMenu {
let imp = self.imp();
imp.sender_context_menu.get_or_init(|| {
let popover = gtk::PopoverMenu::builder()
.has_arrow(false)
.halign(gtk::Align::Start)
.menu_model(&*imp.sender_menu_model)
.build();
popover.update_property(&[gtk::accessible::Property::Label(&gettext(
"Sender Context Menu",
))]);
popover
})
}
fn scroll_to_event(&self, key: &EventKey) {
let room = match self.room() {
Some(room) => room,

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

@ -35,6 +35,82 @@
</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">_Permalink</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, '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>
<attribute name="label" translatable="yes">_Deny Access</attribute>
<attribute name="action">sender-avatar.deny-access</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">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>

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

@ -0,0 +1,634 @@
use adw::subclass::prelude::*;
use gettextrs::gettext;
use gtk::{gdk, glib, glib::clone, prelude::*, CompositeTemplate};
use ruma::events::room::power_levels::PowerLevelAction;
use crate::{
components::Avatar,
gettext_f,
prelude::*,
session::{
model::{Member, Membership, PowerLevelUserAction, Room, User},
view::{content::RoomHistory, user_profile_dialog::UserProfileDialog},
},
toast,
utils::{
add_activate_binding_action, message_dialog::confirm_room_member_destructive_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 user_id_btn: TemplateChild<gtk::Button>,
/// Whether this avatar is active.
///
/// This avatar is active when the popover is displayed.
#[property(get)]
pub active: Cell<bool>,
/// The room of the member.
#[property(get, set = Self::set_room, explicit_notify, nullable)]
pub room: RefCell<Option<Room>>,
pub permissions_handler: RefCell<Option<glib::SignalHandlerId>>,
/// The displayed member.
#[property(get, set = Self::set_sender, explicit_notify, nullable)]
pub sender: BoundObject<Member>,
/// The popover of this avatar.
pub(super) popover: BoundObject<gtk::PopoverMenu>,
}
#[glib::object_subclass]
impl ObjectSubclass for SenderAvatar {
const NAME: &'static str = "ContentSenderAvatar";
type Type = super::SenderAvatar;
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
Avatar::static_type();
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
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("Permalink 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.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.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);
}
}
}
impl WidgetImpl for SenderAvatar {}
impl BinImpl for SenderAvatar {}
impl AccessibleImpl for SenderAvatar {
fn first_accessible_child(&self) -> Option<gtk::Accessible> {
// Hide the children in the a11y tree.
None
}
}
impl SenderAvatar {
/// Set the room of the member.
fn set_room(&self, room: Option<Room>) {
if *self.room.borrow() == room {
return;
}
if let Some(room) = self.room.take() {
if let Some(handler) = self.permissions_handler.take() {
room.permissions().disconnect(handler);
}
}
if let Some(room) = room {
let permissions_handler =
room.permissions()
.connect_changed(clone!(@weak self as imp => move |_| {
imp.update_actions();
}));
self.permissions_handler.replace(Some(permissions_handler));
self.room.replace(Some(room));
self.update_actions();
}
self.obj().notify_room();
}
/// Set the list of room members.
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 display_name_handler =
sender.connect_display_name_notify(clone!(@weak self as imp => move |_| {
imp.update_accessible_label();
}));
let membership_handler =
sender.connect_membership_notify(clone!(@weak self as imp => move |_| {
imp.update_actions();
}));
let power_level_handler =
sender.connect_power_level_notify(clone!(@weak self as imp => move |_| {
imp.update_actions();
}));
let is_ignored_handler =
sender.connect_is_ignored_notify(clone!(@weak self as imp => 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();
}
/// 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(room) = self.room.borrow().clone() else {
return;
};
let Some(sender) = self.sender.obj() else {
return;
};
let obj = self.obj();
let permissions = room.permissions();
let membership = sender.membership();
let sender_id = sender.user_id();
let is_own_user = sender.is_own_user();
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.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
&& matches!(
membership,
Membership::Join | Membership::Invite | Membership::Knock
)
&& 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.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();
// Reset the state.
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 {
// 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 obj => move |popover| {
if !popover.parent().is_some_and(|w| w == obj) {
obj.imp().popover.disconnect_signals();
}
}));
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! {
/// An avatar with a popover menu for room members.
pub struct SenderAvatar(ObjectSubclass<imp::SenderAvatar>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
#[gtk::template_callbacks]
impl SenderAvatar {
pub fn new() -> Self {
glib::Object::new()
}
/// Set whether this active is active.
fn set_active(&self, active: bool) {
if self.active() == active {
return;
}
self.imp().active.set(active);
self.notify_active();
self.set_pressed_state(active);
}
/// Set the CSS and a11 states.
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)]);
}
/// 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
.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();
}
/// Add a mention of the sender to the message composer.
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);
}
/// View the sender details.
fn view_details(&self) {
let Some(sender) = self.sender() else {
return;
};
let Some(room) = self.room() else {
return;
};
let dialog = UserProfileDialog::new(self.root().and_downcast_ref::<gtk::Window>());
dialog.set_room_member(room, sender);
dialog.present();
}
/// 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().and_upcast::<User>() else {
return;
};
let room = if let Some(room) = sender.direct_chat() {
room
} else {
toast!(self, &gettext("Creating a new Direct Chat…"));
match sender.get_or_create_direct_chat().await {
Ok(room) => room,
Err(_) => {
toast!(self, &gettext("Failed to 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());
}
/// Invite the sender to the room.
async fn invite(&self) {
let Some(room) = self.room() else {
return;
};
let Some(sender) = self.sender() else {
return;
};
toast!(self, gettext("Inviting user…"));
let user_id = sender.user_id().clone();
if room.invite(&[user_id]).await.is_err() {
toast!(self, gettext("Failed to invite user"));
}
}
/// Kick the user from the room.
async fn kick(&self) {
let Some(room) = self.room() else {
return;
};
let Some(sender) = self.sender() else {
return;
};
let Some(window) = self.root().and_downcast::<gtk::Window>() else {
return;
};
let (confirmed, reason) =
confirm_room_member_destructive_action(&room, &sender, PowerLevelAction::Kick, &window)
.await;
if !confirmed {
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 user_id = sender.user_id().clone();
if room.kick(&[(user_id, reason)]).await.is_err() {
let error = match membership {
Membership::Invite => gettext("Failed to revoke invite of user"),
Membership::Knock => gettext("Failed to deny access to user"),
_ => gettext("Failed to kick user"),
};
toast!(self, error);
}
}
/// Ban the room member.
async fn ban(&self) {
let Some(room) = self.room() else {
return;
};
let Some(sender) = self.sender() else {
return;
};
let Some(window) = self.root().and_downcast::<gtk::Window>() else {
return;
};
let (confirmed, reason) =
confirm_room_member_destructive_action(&room, &sender, PowerLevelAction::Ban, &window)
.await;
if !confirmed {
return;
}
toast!(self, gettext("Banning user…"));
let user_id = sender.user_id().clone();
if room.ban(&[(user_id, reason)]).await.is_err() {
toast!(self, gettext("Failed to ban user"));
}
}
/// Unban the room member.
async fn unban(&self) {
let Some(room) = self.room() else {
return;
};
let Some(sender) = self.sender() else {
return;
};
toast!(self, gettext("Unbanning user…"));
let user_id = sender.user_id().clone();
if room.unban(&[(user_id, None)]).await.is_err() {
toast!(self, gettext("Failed to unban user"));
}
}
/// Toggle whether the user is ignored or not.
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("Failed to stop ignoring user"));
}
} else if sender.ignore().await.is_err() {
toast!(self, gettext("Failed to ignore user"));
}
}
}

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

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="ContentSenderAvatar" parent="AdwBin">
<accessibility>
<property name="description" translatable="yes">Open Sender Context Menu</property>
</accessibility>
<property name="valign">start</property>
<child>
<object class="ComponentsAvatar" 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">
<binding name="label">
<lookup name="user-id-string">
<lookup name="sender">ContentSenderAvatar</lookup>
</lookup>
</binding>
<property name="tooltip-text" translatable="yes">Copy Matrix user ID to clipboard</property>
<property name="action-name">sender-avatar.copy-user-id</property>
<property name="can-shrink">True</property>
<style>
<class name="flat"/>
</style>
</object>
</interface>

9
src/session/view/create_dm_dialog/dm_user.rs

@ -1,10 +1,9 @@
use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*};
use gtk::{glib, prelude::*, subclass::prelude::*};
use matrix_sdk::ruma::{OwnedMxcUri, OwnedUserId};
use crate::{
prelude::*,
session::model::{Room, Session, User},
spawn,
};
mod imp {
@ -31,10 +30,8 @@ mod imp {
self.parent_constructed();
let obj = self.obj();
spawn!(clone!(@weak obj => async move {
let direct_chat = obj.upcast_ref::<User>().direct_chat().await;
obj.set_direct_chat(direct_chat);
}));
let direct_chat = obj.upcast_ref::<User>().direct_chat();
obj.set_direct_chat(direct_chat);
}
}

3
src/session/view/session_view.rs

@ -341,7 +341,8 @@ impl SessionView {
return;
};
let dialog = UserProfileDialog::new(self.parent_window().as_ref(), &session, user_id);
let dialog = UserProfileDialog::new(self.parent_window().as_ref());
dialog.load_user(&session, user_id);
dialog.present();
}
}

49
src/session/view/user_profile_dialog.rs

@ -6,7 +6,7 @@ use super::UserPage;
use crate::{
components::{Spinner, ToastableWindow},
prelude::*,
session::model::{RemoteUser, Session},
session::model::{Member, RemoteUser, Room, Session, User},
spawn,
};
@ -54,19 +54,6 @@ mod imp {
impl WindowImpl for UserProfileDialog {}
impl AdwWindowImpl for UserProfileDialog {}
impl ToastableWindowImpl for UserProfileDialog {}
impl UserProfileDialog {
/// Load the user with the given session and user ID.
pub fn load_user(&self, session: &Session, user_id: OwnedUserId) {
let user = RemoteUser::new(session, user_id);
self.user_page.set_user(Some(user.clone()));
spawn!(clone!(@weak self as imp, @weak user => async move {
user.load_profile().await;
imp.stack.set_visible_child_name("details");
}));
}
}
}
glib::wrapper! {
@ -77,16 +64,32 @@ glib::wrapper! {
#[gtk::template_callbacks]
impl UserProfileDialog {
pub fn new(
parent_window: Option<&impl IsA<gtk::Window>>,
session: &Session,
user_id: OwnedUserId,
) -> Self {
let obj = glib::Object::builder::<Self>()
/// Create a new `UserProfileDialog`.
pub fn new(parent_window: Option<&impl IsA<gtk::Window>>) -> Self {
glib::Object::builder::<Self>()
.property("transient-for", parent_window)
.build();
.build()
}
/// Load the user with the given session and user ID.
pub fn load_user(&self, session: &Session, user_id: OwnedUserId) {
let imp = self.imp();
let user = RemoteUser::new(session, user_id);
imp.user_page.set_user(Some(user.clone()));
spawn!(clone!(@weak imp, @weak user => async move {
user.load_profile().await;
imp.stack.set_visible_child_name("details");
}));
}
/// Set the member to present.
pub fn set_room_member(&self, room: Room, member: Member) {
let imp = self.imp();
obj.imp().load_user(session, user_id);
obj
imp.user_page.set_room(Some(room));
imp.user_page.set_user(Some(member.upcast::<User>()));
imp.stack.set_visible_child_name("details");
}
}

1
src/ui-resources.gresource.xml

@ -87,6 +87,7 @@
<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_row/creation.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/state_row/mod.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/state_row/tombstone.ui</file>

Loading…
Cancel
Save