1use adw::{prelude::*, subclass::prelude::*};
2use gettextrs::{gettext, ngettext};
3use gtk::{CompositeTemplate, gdk, glib, glib::clone};
4use ruma::{OwnedEventId, events::room::power_levels::PowerLevelUserAction};
5
6use crate::{
7 Window,
8 components::{
9 Avatar, RoomMemberDestructiveAction, UserProfileDialog, confirm_mute_room_member_dialog,
10 confirm_room_member_destructive_action_dialog,
11 },
12 gettext_f,
13 prelude::*,
14 session::{
15 model::{Member, MemberRole, Membership, User},
16 view::content::RoomHistory,
17 },
18 toast,
19 utils::{BoundObject, key_bindings},
20};
21
22mod imp {
23 use std::cell::{Cell, RefCell};
24
25 use glib::subclass::InitializingObject;
26
27 use super::*;
28
29 #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
30 #[template(
31 resource = "/org/gnome/Fractal/ui/session/view/content/room_history/sender_avatar/mod.ui"
32 )]
33 #[properties(wrapper_type = super::SenderAvatar)]
34 pub struct SenderAvatar {
35 #[template_child]
36 avatar: TemplateChild<Avatar>,
37 #[template_child]
38 user_id_btn: TemplateChild<gtk::Button>,
39 #[property(get)]
43 active: Cell<bool>,
44 direct_member_handler: RefCell<Option<glib::SignalHandlerId>>,
45 permissions_handler: RefCell<Option<glib::SignalHandlerId>>,
46 #[property(get, set = Self::set_sender, explicit_notify, nullable)]
48 sender: BoundObject<Member>,
49 popover: BoundObject<gtk::PopoverMenu>,
51 }
52
53 #[glib::object_subclass]
54 impl ObjectSubclass for SenderAvatar {
55 const NAME: &'static str = "ContentSenderAvatar";
56 type Type = super::SenderAvatar;
57 type ParentType = gtk::Widget;
58
59 fn class_init(klass: &mut Self::Class) {
60 Self::bind_template(klass);
61 Self::bind_template_callbacks(klass);
62
63 klass.set_layout_manager_type::<gtk::BinLayout>();
64 klass.set_css_name("sender-avatar");
65 klass.set_accessible_role(gtk::AccessibleRole::ToggleButton);
66
67 klass.install_action("sender-avatar.copy-user-id", None, |obj, _, _| {
68 if let Some(popover) = obj.imp().popover.obj() {
69 popover.popdown();
70 }
71
72 let Some(sender) = obj.sender() else {
73 return;
74 };
75
76 obj.clipboard().set_text(sender.user_id().as_str());
77 toast!(obj, gettext("Matrix user ID copied to clipboard"));
78 });
79
80 klass.install_action("sender-avatar.mention", None, |obj, _, _| {
81 obj.imp().mention();
82 });
83
84 klass.install_action_async(
85 "sender-avatar.open-direct-chat",
86 None,
87 |obj, _, _| async move {
88 obj.imp().open_direct_chat().await;
89 },
90 );
91
92 klass.install_action("sender-avatar.permalink", None, |obj, _, _| {
93 let Some(sender) = obj.sender() else {
94 return;
95 };
96
97 obj.clipboard()
98 .set_text(&sender.matrix_to_uri().to_string());
99 toast!(obj, gettext("Link copied to clipboard"));
100 });
101
102 klass.install_action_async("sender-avatar.invite", None, |obj, _, _| async move {
103 obj.imp().invite().await;
104 });
105
106 klass.install_action_async(
107 "sender-avatar.revoke-invite",
108 None,
109 |obj, _, _| async move {
110 obj.imp().kick().await;
111 },
112 );
113
114 klass.install_action_async("sender-avatar.mute", None, |obj, _, _| async move {
115 obj.imp().toggle_muted().await;
116 });
117
118 klass.install_action_async("sender-avatar.unmute", None, |obj, _, _| async move {
119 obj.imp().toggle_muted().await;
120 });
121
122 klass.install_action_async("sender-avatar.kick", None, |obj, _, _| async move {
123 obj.imp().kick().await;
124 });
125
126 klass.install_action_async("sender-avatar.deny-access", None, |obj, _, _| async move {
127 obj.imp().kick().await;
128 });
129
130 klass.install_action_async("sender-avatar.ban", None, |obj, _, _| async move {
131 obj.imp().ban().await;
132 });
133
134 klass.install_action_async("sender-avatar.unban", None, |obj, _, _| async move {
135 obj.imp().unban().await;
136 });
137
138 klass.install_action_async(
139 "sender-avatar.remove-messages",
140 None,
141 |obj, _, _| async move {
142 obj.imp().remove_messages().await;
143 },
144 );
145
146 klass.install_action_async("sender-avatar.ignore", None, |obj, _, _| async move {
147 obj.imp().toggle_ignored().await;
148 });
149
150 klass.install_action_async(
151 "sender-avatar.stop-ignoring",
152 None,
153 |obj, _, _| async move {
154 obj.imp().toggle_ignored().await;
155 },
156 );
157
158 klass.install_action("sender-avatar.view-details", None, |obj, _, _| {
159 obj.imp().view_details();
160 });
161
162 klass.install_action("sender-avatar.activate", None, |obj, _, _| {
163 obj.imp().show_popover(1, 0.0, 0.0);
164 });
165
166 key_bindings::add_activate_bindings(klass, "sender-avatar.activate");
167 }
168
169 fn instance_init(obj: &InitializingObject<Self>) {
170 obj.init_template();
171 }
172 }
173
174 #[glib::derived_properties]
175 impl ObjectImpl for SenderAvatar {
176 fn constructed(&self) {
177 self.parent_constructed();
178
179 self.set_pressed_state(false);
180 }
181
182 fn dispose(&self) {
183 self.disconnect_signals();
184
185 if let Some(popover) = self.popover.obj() {
186 popover.unparent();
187 popover.remove_child(&*self.user_id_btn);
188 }
189
190 self.avatar.unparent();
191 }
192 }
193
194 impl WidgetImpl for SenderAvatar {}
195
196 impl AccessibleImpl for SenderAvatar {
197 fn first_accessible_child(&self) -> Option<gtk::Accessible> {
198 None
200 }
201 }
202
203 #[gtk::template_callbacks]
204 impl SenderAvatar {
205 fn set_sender(&self, sender: Option<Member>) {
207 let prev_sender = self.sender.obj();
208
209 if prev_sender == sender {
210 return;
211 }
212
213 self.disconnect_signals();
214
215 if let Some(sender) = sender {
216 let room = sender.room();
217 let direct_member_handler = room.connect_direct_member_notify(clone!(
218 #[weak(rename_to = imp)]
219 self,
220 move |_| {
221 imp.update_actions();
222 }
223 ));
224 self.direct_member_handler
225 .replace(Some(direct_member_handler));
226
227 let permissions_handler = room.permissions().connect_changed(clone!(
228 #[weak(rename_to = imp)]
229 self,
230 move |_| {
231 imp.update_actions();
232 }
233 ));
234 self.permissions_handler.replace(Some(permissions_handler));
235
236 let display_name_handler = sender.connect_display_name_notify(clone!(
237 #[weak(rename_to = imp)]
238 self,
239 move |_| {
240 imp.update_accessible_label();
241 }
242 ));
243
244 let membership_handler = sender.connect_membership_notify(clone!(
245 #[weak(rename_to = imp)]
246 self,
247 move |_| {
248 imp.update_actions();
249 }
250 ));
251
252 let power_level_handler = sender.connect_power_level_notify(clone!(
253 #[weak(rename_to = imp)]
254 self,
255 move |_| {
256 imp.update_actions();
257 }
258 ));
259
260 let is_ignored_handler = sender.connect_is_ignored_notify(clone!(
261 #[weak(rename_to = imp)]
262 self,
263 move |_| {
264 imp.update_actions();
265 }
266 ));
267
268 self.sender.set(
269 sender,
270 vec![
271 display_name_handler,
272 membership_handler,
273 power_level_handler,
274 is_ignored_handler,
275 ],
276 );
277 self.update_accessible_label();
278 self.update_actions();
279 }
280
281 self.obj().notify_sender();
282 }
283
284 fn disconnect_signals(&self) {
286 if let Some(sender) = self.sender.obj() {
287 let room = sender.room();
288
289 if let Some(handler) = self.direct_member_handler.take() {
290 room.disconnect(handler);
291 }
292 if let Some(handler) = self.permissions_handler.take() {
293 room.permissions().disconnect(handler);
294 }
295 }
296
297 self.sender.disconnect_signals();
298 }
299
300 fn update_accessible_label(&self) {
302 let Some(sender) = self.sender.obj() else {
303 return;
304 };
305
306 let label = gettext_f("{user}’s avatar", &[("user", &sender.display_name())]);
307 self.obj()
308 .update_property(&[gtk::accessible::Property::Label(&label)]);
309 }
310
311 fn update_actions(&self) {
313 let Some(sender) = self.sender.obj() else {
314 return;
315 };
316 let obj = self.obj();
317
318 let room = sender.room();
319 let is_direct_chat = room.direct_member().is_some();
320 let permissions = room.permissions();
321 let membership = sender.membership();
322 let sender_id = sender.user_id();
323 let is_own_user = sender.is_own_user();
324 let power_level = sender.power_level();
325 let role = permissions.role(power_level);
326
327 obj.action_set_enabled(
328 "sender-avatar.mention",
329 !is_own_user && membership == Membership::Join && permissions.can_send_message(),
330 );
331
332 obj.action_set_enabled(
333 "sender-avatar.open-direct-chat",
334 !is_direct_chat && !is_own_user,
335 );
336
337 obj.action_set_enabled(
338 "sender-avatar.invite",
339 !is_own_user
340 && matches!(membership, Membership::Leave | Membership::Knock)
341 && permissions.can_do_to_user(sender_id, PowerLevelUserAction::Kick),
342 );
343
344 obj.action_set_enabled(
345 "sender-avatar.revoke-invite",
346 !is_own_user
347 && membership == Membership::Invite
348 && permissions.can_do_to_user(sender_id, PowerLevelUserAction::Kick),
349 );
350
351 obj.action_set_enabled(
352 "sender-avatar.mute",
353 !is_own_user
354 && role != MemberRole::Muted
355 && permissions.default_power_level() > permissions.mute_power_level()
356 && permissions
357 .can_do_to_user(sender_id, PowerLevelUserAction::ChangePowerLevel),
358 );
359
360 obj.action_set_enabled(
361 "sender-avatar.unmute",
362 !is_own_user
363 && role == MemberRole::Muted
364 && permissions.default_power_level() > permissions.mute_power_level()
365 && permissions
366 .can_do_to_user(sender_id, PowerLevelUserAction::ChangePowerLevel),
367 );
368
369 obj.action_set_enabled(
370 "sender-avatar.kick",
371 !is_own_user
372 && membership == Membership::Join
373 && permissions.can_do_to_user(sender_id, PowerLevelUserAction::Kick),
374 );
375
376 obj.action_set_enabled(
377 "sender-avatar.deny-access",
378 !is_own_user
379 && membership == Membership::Knock
380 && permissions.can_do_to_user(sender_id, PowerLevelUserAction::Kick),
381 );
382
383 obj.action_set_enabled(
384 "sender-avatar.ban",
385 !is_own_user
386 && membership != Membership::Ban
387 && permissions.can_do_to_user(sender_id, PowerLevelUserAction::Ban),
388 );
389
390 obj.action_set_enabled(
391 "sender-avatar.unban",
392 !is_own_user
393 && membership == Membership::Ban
394 && permissions.can_do_to_user(sender_id, PowerLevelUserAction::Unban),
395 );
396
397 obj.action_set_enabled(
398 "sender-avatar.remove-messages",
399 !is_own_user && permissions.can_redact_other(),
400 );
401
402 obj.action_set_enabled("sender-avatar.ignore", !is_own_user && !sender.is_ignored());
403
404 obj.action_set_enabled(
405 "sender-avatar.stop-ignoring",
406 !is_own_user && sender.is_ignored(),
407 );
408 }
409
410 fn set_popover(&self, popover: Option<gtk::PopoverMenu>) {
412 let old_popover = self.popover.obj();
413
414 if old_popover == popover {
415 return;
416 }
417
418 if let Some(popover) = old_popover {
420 popover.unparent();
421 popover.remove_child(&*self.user_id_btn);
422 }
423 self.popover.disconnect_signals();
424 self.set_active(false);
425
426 if let Some(popover) = popover {
427 if popover.parent().is_some() {
429 popover.unparent();
430 }
431
432 let parent_handler = popover.connect_parent_notify(clone!(
433 #[weak(rename_to = imp)]
434 self,
435 move |popover| {
436 if popover.parent().is_none_or(|w| w != *imp.obj()) {
437 imp.popover.disconnect_signals();
438 popover.remove_child(&*imp.user_id_btn);
439 }
440 }
441 ));
442 let closed_handler = popover.connect_closed(clone!(
443 #[weak(rename_to = imp)]
444 self,
445 move |_| {
446 imp.set_active(false);
447 }
448 ));
449
450 popover.add_child(&*self.user_id_btn, "user-id");
451 popover.set_parent(&*self.obj());
452
453 self.popover
454 .set(popover, vec![parent_handler, closed_handler]);
455 }
456 }
457
458 fn set_active(&self, active: bool) {
460 if self.active.get() == active {
461 return;
462 }
463
464 self.active.set(active);
465
466 self.obj().notify_active();
467 self.set_pressed_state(active);
468 }
469
470 fn set_pressed_state(&self, pressed: bool) {
472 let obj = self.obj();
473
474 if pressed {
475 obj.set_state_flags(gtk::StateFlags::CHECKED, false);
476 } else {
477 obj.unset_state_flags(gtk::StateFlags::CHECKED);
478 }
479
480 let tristate = if pressed {
481 gtk::AccessibleTristate::True
482 } else {
483 gtk::AccessibleTristate::False
484 };
485 obj.update_state(&[gtk::accessible::State::Pressed(tristate)]);
486 }
487
488 fn room_history(&self) -> Option<RoomHistory> {
490 self.obj()
491 .ancestor(RoomHistory::static_type())
492 .and_downcast()
493 }
494
495 #[template_callback]
499 fn show_popover(&self, _n_press: i32, x: f64, y: f64) {
500 let Some(room_history) = self.room_history() else {
501 return;
502 };
503
504 self.set_active(true);
505
506 let popover = room_history.sender_context_menu();
507 self.set_popover(Some(popover.clone()));
508
509 popover.set_pointing_to(Some(&gdk::Rectangle::new(x as i32, y as i32, 0, 0)));
510 popover.popup();
511 }
512
513 fn mention(&self) {
515 let Some(sender) = self.sender.obj() else {
516 return;
517 };
518 let Some(room_history) = self.room_history() else {
519 return;
520 };
521
522 room_history.message_toolbar().mention_member(&sender);
523 }
524
525 fn view_details(&self) {
527 let Some(sender) = self.sender.obj() else {
528 return;
529 };
530
531 let dialog = UserProfileDialog::new();
532 dialog.set_room_member(sender);
533 dialog.present(Some(&*self.obj()));
534 }
535
536 async fn open_direct_chat(&self) {
540 let Some(sender) = self.sender.obj().and_upcast::<User>() else {
541 return;
542 };
543 let obj = self.obj();
544
545 let room = if let Some(room) = sender.direct_chat() {
546 room
547 } else {
548 toast!(obj, &gettext("Creating a new Direct Chat…"));
549
550 if let Ok(room) = sender.get_or_create_direct_chat().await {
551 room
552 } else {
553 toast!(obj, &gettext("Could not create a new Direct Chat"));
554 return;
555 }
556 };
557
558 let Some(main_window) = obj.root().and_downcast::<Window>() else {
559 return;
560 };
561
562 main_window.session_view().select_room(room);
563 }
564
565 async fn invite(&self) {
567 let Some(sender) = self.sender.obj() else {
568 return;
569 };
570 let obj = self.obj();
571
572 toast!(obj, gettext("Inviting user…"));
573
574 let room = sender.room();
575 let user_id = sender.user_id().clone();
576 if room.invite(&[user_id]).await.is_err() {
577 toast!(obj, gettext("Could not invite user"));
578 }
579 }
580
581 async fn kick(&self) {
583 let Some(sender) = self.sender.obj() else {
584 return;
585 };
586 let obj = self.obj();
587
588 let Some(response) = confirm_room_member_destructive_action_dialog(
589 &sender,
590 RoomMemberDestructiveAction::Kick,
591 &*obj,
592 )
593 .await
594 else {
595 return;
596 };
597
598 let membership = sender.membership();
599
600 let label = match membership {
601 Membership::Invite => gettext("Revoking invite…"),
602 Membership::Knock => gettext("Denying access…"),
603 _ => gettext("Kicking user…"),
604 };
605 toast!(obj, label);
606
607 let room = sender.room();
608 let user_id = sender.user_id().clone();
609 if room.kick(&[(user_id, response.reason)]).await.is_err() {
610 let error = match membership {
611 Membership::Invite => gettext("Could not revoke invite of user"),
612 Membership::Knock => gettext("Could not deny access to user"),
613 _ => gettext("Could not kick user"),
614 };
615 toast!(obj, error);
616 }
617 }
618
619 async fn toggle_muted(&self) {
621 let Some(sender) = self.sender.obj() else {
622 return;
623 };
624 let obj = self.obj();
625
626 let old_power_level = sender.power_level();
627 let permissions = sender.room().permissions();
628
629 let mute_power_level = permissions.mute_power_level();
631 let mute = old_power_level > mute_power_level;
632 if mute && !confirm_mute_room_member_dialog(&sender, &*obj).await {
633 return;
634 }
635
636 let user_id = sender.user_id().clone();
637
638 let (new_power_level, text) = if mute {
639 (mute_power_level, gettext("Muting member…"))
640 } else {
641 (
642 permissions.default_power_level(),
643 gettext("Unmuting member…"),
644 )
645 };
646 toast!(obj, text);
647
648 let text = if permissions
649 .set_user_power_level(user_id, new_power_level)
650 .await
651 .is_ok()
652 {
653 if mute {
654 gettext("Member muted")
655 } else {
656 gettext("Member unmuted")
657 }
658 } else if mute {
659 gettext("Could not mute member")
660 } else {
661 gettext("Could not unmute member")
662 };
663 toast!(obj, text);
664 }
665
666 async fn ban(&self) {
668 let Some(sender) = self.sender.obj() else {
669 return;
670 };
671 let obj = self.obj();
672
673 let permissions = sender.room().permissions();
674 let redactable_events = if permissions.can_redact_other() {
675 sender.redactable_events()
676 } else {
677 vec![]
678 };
679
680 let Some(response) = confirm_room_member_destructive_action_dialog(
681 &sender,
682 RoomMemberDestructiveAction::Ban(redactable_events.len()),
683 &*obj,
684 )
685 .await
686 else {
687 return;
688 };
689
690 toast!(obj, gettext("Banning user…"));
691
692 let room = sender.room();
693 let user_id = sender.user_id().clone();
694 if room
695 .ban(&[(user_id, response.reason.clone())])
696 .await
697 .is_err()
698 {
699 toast!(obj, gettext("Could not ban user"));
700 }
701
702 if response.remove_events {
703 self.remove_known_messages_inner(&sender, redactable_events, response.reason)
704 .await;
705 }
706 }
707
708 async fn unban(&self) {
710 let Some(sender) = self.sender.obj() else {
711 return;
712 };
713 let obj = self.obj();
714
715 toast!(obj, gettext("Unbanning user…"));
716
717 let room = sender.room();
718 let user_id = sender.user_id().clone();
719 if room.unban(&[(user_id, None)]).await.is_err() {
720 toast!(obj, gettext("Could not unban user"));
721 }
722 }
723
724 async fn remove_messages(&self) {
726 let Some(sender) = self.sender.obj() else {
727 return;
728 };
729
730 let redactable_events = sender.redactable_events();
731
732 let Some(response) = confirm_room_member_destructive_action_dialog(
733 &sender,
734 RoomMemberDestructiveAction::RemoveMessages(redactable_events.len()),
735 &*self.obj(),
736 )
737 .await
738 else {
739 return;
740 };
741
742 self.remove_known_messages_inner(&sender, redactable_events, response.reason)
743 .await;
744 }
745
746 async fn remove_known_messages_inner(
747 &self,
748 sender: &Member,
749 events: Vec<OwnedEventId>,
750 reason: Option<String>,
751 ) {
752 let obj = self.obj();
753 let n = u32::try_from(events.len()).unwrap_or(u32::MAX);
754 toast!(
755 obj,
756 ngettext(
757 "Removing 1 message sent by the user…",
760 "Removing {n} messages sent by the user…",
761 n,
762 ),
763 n,
764 );
765
766 let room = sender.room();
767
768 if let Err(failed_events) = room.redact(&events, reason).await {
769 let n = u32::try_from(failed_events.len()).unwrap_or(u32::MAX);
770 toast!(
771 obj,
772 ngettext(
773 "Could not remove 1 message sent by the user",
776 "Could not remove {n} messages sent by the user",
777 n,
778 ),
779 n,
780 );
781 }
782 }
783
784 async fn toggle_ignored(&self) {
786 let Some(sender) = self.sender.obj().and_upcast::<User>() else {
787 return;
788 };
789 let obj = self.obj();
790 let is_ignored = sender.is_ignored();
791
792 let label = if is_ignored {
793 gettext("Stop ignoring user…")
794 } else {
795 gettext("Ignoring user…")
796 };
797 toast!(obj, label);
798
799 if is_ignored {
800 if sender.stop_ignoring().await.is_err() {
801 toast!(obj, gettext("Could not stop ignoring user"));
802 }
803 } else if sender.ignore().await.is_err() {
804 toast!(obj, gettext("Could not ignore user"));
805 }
806 }
807 }
808}
809
810glib::wrapper! {
811 pub struct SenderAvatar(ObjectSubclass<imp::SenderAvatar>)
813 @extends gtk::Widget, @implements gtk::Accessible;
814}
815
816impl SenderAvatar {
817 pub fn new() -> Self {
818 glib::Object::new()
819 }
820}