1use std::slice;
2
3use adw::{prelude::*, subclass::prelude::*};
4use gettextrs::{gettext, ngettext, pgettext};
5use gtk::{
6 glib,
7 glib::{clone, closure_local},
8};
9use ruma::{
10 OwnedEventId,
11 events::room::power_levels::{PowerLevelUserAction, UserPowerLevel},
12};
13
14use super::{Avatar, LoadingButton, LoadingButtonRow, PowerLevelSelectionRow};
15use crate::{
16 Window,
17 components::{
18 RoomMemberDestructiveAction, confirm_mute_room_member_dialog, confirm_own_demotion_dialog,
19 confirm_room_member_destructive_action_dialog,
20 confirm_set_room_member_power_level_same_as_own_dialog,
21 },
22 gettext_f,
23 prelude::*,
24 session::{Member, Membership, Permissions, Room, User},
25 toast,
26 utils::BoundObject,
27};
28
29mod imp {
30 use std::{cell::RefCell, sync::LazyLock};
31
32 use glib::subclass::{InitializingObject, Signal};
33
34 use super::*;
35
36 #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
37 #[template(resource = "/org/gnome/Fractal/ui/components/user_page.ui")]
38 #[properties(wrapper_type = super::UserPage)]
39 pub struct UserPage {
40 #[template_child]
41 avatar: TemplateChild<Avatar>,
42 #[template_child]
43 direct_chat_box: TemplateChild<gtk::ListBox>,
44 #[template_child]
45 direct_chat_button: TemplateChild<LoadingButtonRow>,
46 #[template_child]
47 verified_row: TemplateChild<adw::ActionRow>,
48 #[template_child]
49 verified_stack: TemplateChild<gtk::Stack>,
50 #[template_child]
51 verify_button: TemplateChild<LoadingButton>,
52 #[template_child]
53 room_box: TemplateChild<gtk::Box>,
54 #[template_child]
55 room_title: TemplateChild<gtk::Label>,
56 #[template_child]
57 membership_row: TemplateChild<adw::ActionRow>,
58 #[template_child]
59 membership_label: TemplateChild<gtk::Label>,
60 #[template_child]
61 power_level_row: TemplateChild<PowerLevelSelectionRow>,
62 #[template_child]
63 invite_button: TemplateChild<LoadingButtonRow>,
64 #[template_child]
65 kick_button: TemplateChild<LoadingButtonRow>,
66 #[template_child]
67 ban_button: TemplateChild<LoadingButtonRow>,
68 #[template_child]
69 unban_button: TemplateChild<LoadingButtonRow>,
70 #[template_child]
71 remove_messages_button: TemplateChild<LoadingButtonRow>,
72 #[template_child]
73 ignored_row: TemplateChild<adw::ActionRow>,
74 #[template_child]
75 ignored_button: TemplateChild<LoadingButton>,
76 #[property(get, set = Self::set_user, explicit_notify, nullable)]
78 user: BoundObject<User>,
79 bindings: RefCell<Vec<glib::Binding>>,
80 permissions_handler: RefCell<Option<glib::SignalHandlerId>>,
81 room_handlers: RefCell<Vec<glib::SignalHandlerId>>,
82 }
83
84 #[glib::object_subclass]
85 impl ObjectSubclass for UserPage {
86 const NAME: &'static str = "UserPage";
87 type Type = super::UserPage;
88 type ParentType = adw::NavigationPage;
89
90 fn class_init(klass: &mut Self::Class) {
91 Self::bind_template(klass);
92 Self::bind_template_callbacks(klass);
93
94 klass.set_css_name("user-page");
95 }
96
97 fn instance_init(obj: &InitializingObject<Self>) {
98 obj.init_template();
99 }
100 }
101
102 #[glib::derived_properties]
103 impl ObjectImpl for UserPage {
104 fn signals() -> &'static [Signal] {
105 static SIGNALS: LazyLock<Vec<Signal>> =
106 LazyLock::new(|| vec![Signal::builder("close").build()]);
107 SIGNALS.as_ref()
108 }
109
110 fn dispose(&self) {
111 self.disconnect_signals();
112 }
113 }
114
115 impl WidgetImpl for UserPage {}
116 impl NavigationPageImpl for UserPage {}
117
118 #[gtk::template_callbacks]
119 impl UserPage {
120 fn set_user(&self, user: Option<User>) {
122 if self.user.obj() == user {
123 return;
124 }
125 let obj = self.obj();
126
127 self.disconnect_signals();
128 self.power_level_row.set_permissions(None::<Permissions>);
129
130 if let Some(user) = user {
131 let title_binding = user
132 .bind_property("display-name", &*obj, "title")
133 .sync_create()
134 .build();
135 let avatar_binding = user
136 .bind_property("avatar-data", &*self.avatar, "data")
137 .sync_create()
138 .build();
139 let bindings = vec![title_binding, avatar_binding];
140
141 let verified_handler = user.connect_is_verified_notify(clone!(
142 #[weak(rename_to = imp)]
143 self,
144 move |_| {
145 imp.update_verified();
146 }
147 ));
148 let ignored_handler = user.connect_is_ignored_notify(clone!(
149 #[weak(rename_to = imp)]
150 self,
151 move |_| {
152 imp.update_direct_chat();
153 imp.update_ignored();
154 }
155 ));
156 let mut handlers = vec![verified_handler, ignored_handler];
157
158 if let Some(member) = user.downcast_ref::<Member>() {
159 let room = member.room();
160
161 let permissions = room.permissions();
162 let permissions_handler = permissions.connect_changed(clone!(
163 #[weak(rename_to = imp)]
164 self,
165 move |_| {
166 imp.update_room();
167 }
168 ));
169 self.permissions_handler.replace(Some(permissions_handler));
170 self.power_level_row.set_permissions(Some(permissions));
171
172 let room_display_name_handler = room.connect_display_name_notify(clone!(
173 #[weak(rename_to = imp)]
174 self,
175 move |_| {
176 imp.update_room();
177 }
178 ));
179 let room_direct_member_handler = room.connect_direct_member_notify(clone!(
180 #[weak(rename_to = imp)]
181 self,
182 move |_| {
183 imp.update_direct_chat();
184 }
185 ));
186 self.room_handlers
187 .replace(vec![room_display_name_handler, room_direct_member_handler]);
188
189 let membership_handler = member.connect_membership_notify(clone!(
190 #[weak(rename_to = imp)]
191 self,
192 move |member| {
193 if member.membership() == Membership::Leave {
194 imp.obj().emit_by_name::<()>("close", &[]);
195 } else {
196 imp.update_room();
197 }
198 }
199 ));
200 let power_level_handler = member.connect_power_level_changed(clone!(
201 #[weak(rename_to = imp)]
202 self,
203 move |_| {
204 imp.update_room();
205 }
206 ));
207 handlers.extend([membership_handler, power_level_handler]);
208 }
209
210 let is_own_user = user.is_own_user();
213 self.ignored_row.set_visible(!is_own_user);
214
215 self.user.set(user, handlers);
216 self.bindings.replace(bindings);
217 }
218
219 self.load_direct_chat();
220 self.update_direct_chat();
221 self.update_room();
222 self.update_verified();
223 self.update_ignored();
224 obj.notify_user();
225 }
226
227 fn disconnect_signals(&self) {
229 if let Some(member) = self.user.obj().and_downcast::<Member>() {
230 let room = member.room();
231
232 for handler in self.room_handlers.take() {
233 room.disconnect(handler);
234 }
235 if let Some(handler) = self.permissions_handler.take() {
236 room.permissions().disconnect(handler);
237 }
238 }
239
240 for binding in self.bindings.take() {
241 binding.unbind();
242 }
243
244 self.user.disconnect_signals();
245 }
246
247 #[template_callback]
249 fn copy_user_id(&self) {
250 let Some(user) = self.user.obj() else {
251 return;
252 };
253
254 let obj = self.obj();
255 obj.clipboard().set_text(user.user_id().as_str());
256 toast!(obj, gettext("Matrix user ID copied to clipboard"));
257 }
258
259 fn update_direct_chat(&self) {
261 let user = self.user.obj();
262 let is_other_user = user
263 .as_ref()
264 .is_some_and(|u| !u.is_own_user() && !u.is_ignored());
265 let is_direct_chat = user
266 .and_downcast::<Member>()
267 .is_some_and(|m| m.room().direct_member().is_some());
268 self.direct_chat_box
269 .set_visible(is_other_user && !is_direct_chat);
270 }
271
272 fn load_direct_chat(&self) {
274 self.direct_chat_button.set_is_loading(true);
275
276 let Some(user) = self.user.obj() else {
277 return;
278 };
279
280 let direct_chat = user.direct_chat();
281
282 let title = if direct_chat.is_some() {
283 gettext("Open Direct Chat")
284 } else {
285 gettext("Create Direct Chat")
286 };
287 self.direct_chat_button.set_title(&title);
288
289 self.direct_chat_button.set_is_loading(false);
290 }
291
292 #[template_callback]
296 async fn open_direct_chat(&self) {
297 let Some(user) = self.user.obj() else {
298 return;
299 };
300
301 self.direct_chat_button.set_is_loading(true);
302 let obj = self.obj();
303
304 let Ok(room) = user.get_or_create_direct_chat().await else {
305 toast!(obj, &gettext("Could not create a new Direct Chat"));
306 self.direct_chat_button.set_is_loading(false);
307
308 return;
309 };
310
311 let Some(parent_window) = obj.root().and_downcast::<gtk::Window>() else {
312 return;
313 };
314
315 if let Some(main_window) = parent_window.transient_for().and_downcast::<Window>() {
316 main_window.session_view().select_room(room);
317 }
318
319 parent_window.close();
320 }
321
322 fn update_room(&self) {
324 let Some(member) = self.user.obj().and_downcast::<Member>() else {
325 self.room_box.set_visible(false);
326 return;
327 };
328
329 let membership = member.membership();
330 if membership == Membership::Leave {
331 self.room_box.set_visible(false);
332 return;
333 }
334
335 let room = member.room();
336 let room_title = gettext_f("In {room_name}", &[("room_name", &room.display_name())]);
337 self.room_title.set_label(&room_title);
338
339 let label = match membership {
340 Membership::Leave => unreachable!(),
341 Membership::Join => {
342 None
344 }
345 Membership::Invite => {
346 Some(pgettext("member", "Invited"))
348 }
349 Membership::Ban => {
350 Some(pgettext("member", "Banned"))
352 }
353 Membership::Knock => {
354 Some(pgettext("member", "Requested an Invite"))
356 }
357 Membership::Unsupported => {
358 Some(pgettext("member", "Unknown"))
360 }
361 };
362 if let Some(label) = label {
363 self.membership_label.set_label(&label);
364 }
365
366 let is_role = membership == Membership::Join;
367 self.membership_row.set_visible(!is_role);
368 self.power_level_row.set_visible(is_role);
369
370 let permissions = room.permissions();
371 let user_id = member.user_id();
372
373 self.power_level_row.set_is_loading(false);
374 self.power_level_row
375 .set_selected_power_level(member.power_level());
376
377 let can_change_power_level =
378 permissions.can_do_to_user(user_id, PowerLevelUserAction::ChangePowerLevel);
379 self.power_level_row.set_read_only(!can_change_power_level);
380
381 let can_invite = matches!(membership, Membership::Knock) && permissions.can_invite();
382 self.invite_button.set_visible(can_invite);
383
384 let can_kick = matches!(
385 membership,
386 Membership::Join | Membership::Invite | Membership::Knock
387 ) && permissions.can_do_to_user(user_id, PowerLevelUserAction::Kick);
388 if can_kick {
389 let label = match membership {
390 Membership::Invite => gettext("Revoke Invite"),
391 Membership::Knock => gettext("Deny Request"),
392 _ => gettext("Kick"),
394 };
395 self.kick_button.set_title(&label);
396 }
397 self.kick_button.set_visible(can_kick);
398
399 let can_ban = membership != Membership::Ban
400 && permissions.can_do_to_user(user_id, PowerLevelUserAction::Ban);
401 self.ban_button.set_visible(can_ban);
402
403 let can_unban = matches!(membership, Membership::Ban)
404 && permissions.can_do_to_user(user_id, PowerLevelUserAction::Unban);
405 self.unban_button.set_visible(can_unban);
406
407 let can_redact = !member.is_own_user() && permissions.can_redact_other();
408 self.remove_messages_button.set_visible(can_redact);
409
410 self.room_box.set_visible(true);
411 }
412
413 fn reset_room(&self) {
415 self.kick_button.set_is_loading(false);
416 self.kick_button.set_sensitive(true);
417
418 self.invite_button.set_is_loading(false);
419 self.invite_button.set_sensitive(true);
420
421 self.ban_button.set_is_loading(false);
422 self.ban_button.set_sensitive(true);
423
424 self.unban_button.set_is_loading(false);
425 self.unban_button.set_sensitive(true);
426
427 self.remove_messages_button.set_is_loading(false);
428 self.remove_messages_button.set_sensitive(true);
429 }
430
431 #[template_callback]
433 async fn set_power_level(&self) {
434 let Some(member) = self.user.obj().and_downcast::<Member>() else {
435 return;
436 };
437
438 let row = &self.power_level_row;
439 let UserPowerLevel::Int(power_level) = row.selected_power_level() else {
440 return;
442 };
443
444 let UserPowerLevel::Int(old_power_level) = member.power_level() else {
445 return;
447 };
448
449 if old_power_level == power_level {
450 return;
452 }
453
454 row.set_is_loading(true);
455 row.set_read_only(true);
456
457 let obj = self.obj();
458 let permissions = member.room().permissions();
459
460 if member.is_own_user() {
461 if !confirm_own_demotion_dialog(&*obj).await {
463 self.update_room();
464 return;
465 }
466 } else {
467 let mute_power_level = permissions.mute_power_level();
469 let is_muted = i64::from(power_level) <= mute_power_level
470 && i64::from(old_power_level) > mute_power_level;
471 if is_muted
472 && !confirm_mute_room_member_dialog(slice::from_ref(&member), &*obj).await
473 {
474 self.update_room();
475 return;
476 }
477
478 let is_own_power_level = power_level == permissions.own_power_level();
480 if is_own_power_level
481 && !confirm_set_room_member_power_level_same_as_own_dialog(
482 slice::from_ref(&member),
483 &*obj,
484 )
485 .await
486 {
487 self.update_room();
488 return;
489 }
490 }
491
492 let user_id = member.user_id().clone();
493
494 if permissions
495 .set_user_power_level(user_id, power_level)
496 .await
497 .is_err()
498 {
499 toast!(obj, gettext("Could not change the role"));
500 self.update_room();
501 }
502 }
503
504 #[template_callback]
506 async fn invite_user(&self) {
507 let Some(member) = self.user.obj().and_downcast::<Member>() else {
508 return;
509 };
510
511 self.invite_button.set_is_loading(true);
512 self.kick_button.set_sensitive(false);
513 self.ban_button.set_sensitive(false);
514 self.unban_button.set_sensitive(false);
515
516 let room = member.room();
517 let user_id = member.user_id().clone();
518
519 if room.invite(&[user_id]).await.is_err() {
520 toast!(self.obj(), gettext("Could not invite user"));
521 }
522
523 self.reset_room();
524 }
525
526 #[template_callback]
528 async fn kick_user(&self) {
529 let Some(member) = self.user.obj().and_downcast::<Member>() else {
530 return;
531 };
532 let obj = self.obj();
533
534 self.kick_button.set_is_loading(true);
535 self.invite_button.set_sensitive(false);
536 self.ban_button.set_sensitive(false);
537 self.unban_button.set_sensitive(false);
538
539 let Some(response) = confirm_room_member_destructive_action_dialog(
540 &member,
541 RoomMemberDestructiveAction::Kick,
542 &*obj,
543 )
544 .await
545 else {
546 self.reset_room();
547 return;
548 };
549
550 let room = member.room();
551 let user_id = member.user_id().clone();
552 if room.kick(&[(user_id, response.reason)]).await.is_err() {
553 let error = match member.membership() {
554 Membership::Invite => gettext("Could not revoke invite of user"),
555 Membership::Knock => gettext("Could not deny access to user"),
556 _ => gettext("Could not kick user"),
557 };
558 toast!(obj, error);
559
560 self.reset_room();
561 }
562 }
563
564 #[template_callback]
566 async fn ban_user(&self) {
567 let Some(member) = self.user.obj().and_downcast::<Member>() else {
568 return;
569 };
570 let obj = self.obj();
571
572 self.ban_button.set_is_loading(true);
573 self.invite_button.set_sensitive(false);
574 self.kick_button.set_sensitive(false);
575 self.unban_button.set_sensitive(false);
576
577 let permissions = member.room().permissions();
578 let redactable_events = if permissions.can_redact_other() {
579 member.redactable_events()
580 } else {
581 vec![]
582 };
583
584 let Some(response) = confirm_room_member_destructive_action_dialog(
585 &member,
586 RoomMemberDestructiveAction::Ban(redactable_events.len()),
587 &*obj,
588 )
589 .await
590 else {
591 self.reset_room();
592 return;
593 };
594
595 let room = member.room();
596 let user_id = member.user_id().clone();
597 if room
598 .ban(&[(user_id, response.reason.clone())])
599 .await
600 .is_err()
601 {
602 toast!(obj, gettext("Could not ban user"));
603 }
604
605 if response.remove_events {
606 self.remove_known_messages_inner(
607 &member.room(),
608 redactable_events,
609 response.reason,
610 )
611 .await;
612 }
613
614 self.reset_room();
615 }
616
617 #[template_callback]
619 async fn unban_user(&self) {
620 let Some(member) = self.user.obj().and_downcast::<Member>() else {
621 return;
622 };
623
624 self.unban_button.set_is_loading(true);
625 self.invite_button.set_sensitive(false);
626 self.kick_button.set_sensitive(false);
627 self.ban_button.set_sensitive(false);
628
629 let room = member.room();
630 let user_id = member.user_id().clone();
631
632 if room.unban(&[(user_id, None)]).await.is_err() {
633 toast!(self.obj(), gettext("Could not unban user"));
634 }
635
636 self.reset_room();
637 }
638
639 #[template_callback]
641 async fn remove_messages(&self) {
642 let Some(member) = self.user.obj().and_downcast::<Member>() else {
643 return;
644 };
645
646 self.remove_messages_button.set_is_loading(true);
647
648 let redactable_events = member.redactable_events();
649
650 let Some(response) = confirm_room_member_destructive_action_dialog(
651 &member,
652 RoomMemberDestructiveAction::RemoveMessages(redactable_events.len()),
653 &*self.obj(),
654 )
655 .await
656 else {
657 self.reset_room();
658 return;
659 };
660
661 self.remove_known_messages_inner(&member.room(), redactable_events, response.reason)
662 .await;
663
664 self.reset_room();
665 }
666
667 async fn remove_known_messages_inner(
668 &self,
669 room: &Room,
670 events: Vec<OwnedEventId>,
671 reason: Option<String>,
672 ) {
673 if let Err(events) = room.redact(&events, reason).await {
674 let n = u32::try_from(events.len()).unwrap_or(u32::MAX);
675
676 toast!(
677 self.obj(),
678 ngettext(
679 "Could not remove 1 message sent by the user",
682 "Could not remove {n} messages sent by the user",
683 n,
684 ),
685 n,
686 );
687 }
688 }
689
690 fn update_verified(&self) {
692 let Some(user) = self.user.obj() else {
693 return;
694 };
695
696 if user.is_verified() {
697 self.verified_row.set_title(&gettext("Identity verified"));
698 self.verified_stack.set_visible_child_name("icon");
699 self.verify_button.set_sensitive(false);
700 } else {
701 self.verify_button.set_sensitive(true);
702 self.verified_stack.set_visible_child_name("button");
703 self.verified_row
704 .set_title(&gettext("Identity not verified"));
705 }
706 }
707
708 #[template_callback]
710 async fn verify_user(&self) {
711 let Some(user) = self.user.obj() else {
712 return;
713 };
714 let obj = self.obj();
715
716 self.verify_button.set_is_loading(true);
717
718 let Ok(verification) = user.verify_identity().await else {
719 toast!(obj, gettext("Could not start user verification"));
720 self.verify_button.set_is_loading(false);
721 return;
722 };
723
724 let Some(parent_window) = obj.root().and_downcast::<gtk::Window>() else {
725 return;
726 };
727
728 if let Some(main_window) = parent_window.transient_for().and_downcast::<Window>() {
729 main_window
730 .session_view()
731 .select_identity_verification(verification);
732 }
733
734 parent_window.close();
735 }
736
737 fn update_ignored(&self) {
739 let Some(user) = self.user.obj() else {
740 return;
741 };
742
743 if user.is_ignored() {
744 self.ignored_row.set_title(&gettext("Ignored"));
745 self.ignored_button
746 .set_content_label(gettext("Stop Ignoring"));
747 self.ignored_button.remove_css_class("destructive-action");
748 } else {
749 self.ignored_row.set_title(&gettext("Not Ignored"));
750 self.ignored_button.set_content_label(gettext("Ignore"));
751 self.ignored_button.add_css_class("destructive-action");
752 }
753 }
754
755 #[template_callback]
757 async fn toggle_ignored(&self) {
758 let Some(user) = self.user.obj() else {
759 return;
760 };
761
762 let obj = self.obj();
763 self.ignored_button.set_is_loading(true);
764
765 if user.is_ignored() {
766 if user.stop_ignoring().await.is_err() {
767 toast!(obj, gettext("Could not stop ignoring user"));
768 }
769 } else if user.ignore().await.is_err() {
770 toast!(obj, gettext("Could not ignore user"));
771 }
772
773 self.ignored_button.set_is_loading(false);
774 }
775 }
776}
777
778glib::wrapper! {
779 pub struct UserPage(ObjectSubclass<imp::UserPage>)
781 @extends gtk::Widget, adw::NavigationPage,
782 @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
783}
784
785impl UserPage {
786 pub fn new(user: &impl IsA<User>) -> Self {
788 glib::Object::builder().property("user", user).build()
789 }
790
791 pub fn connect_close<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
793 self.connect_closure(
794 "close",
795 true,
796 closure_local!(|obj: Self| {
797 f(&obj);
798 }),
799 )
800 }
801}