1use adw::{prelude::*, subclass::prelude::*};
2use gettextrs::{gettext, ngettext};
3use gtk::{
4 gio,
5 glib::{self, clone},
6 pango, CompositeTemplate,
7};
8use ruma::{
9 api::client::{
10 directory::{get_room_visibility, set_room_visibility},
11 discovery::get_capabilities::Capabilities,
12 room::{upgrade_room, Visibility},
13 },
14 events::{
15 room::{
16 guest_access::{GuestAccess, RoomGuestAccessEventContent},
17 history_visibility::RoomHistoryVisibilityEventContent,
18 power_levels::PowerLevelAction,
19 },
20 StateEventType,
21 },
22};
23use tracing::error;
24
25use super::{room_upgrade_dialog::confirm_room_upgrade, MemberRow, MembershipLists, RoomDetails};
26use crate::{
27 components::{
28 Avatar, ButtonCountRow, CheckLoadingRow, ComboLoadingRow, CopyableRow, LoadingButton,
29 SwitchLoadingRow,
30 },
31 gettext_f,
32 prelude::*,
33 session::model::{
34 HistoryVisibilityValue, JoinRuleValue, Member, NotificationsRoomSetting, Room, RoomCategory,
35 },
36 spawn, spawn_tokio, toast,
37 utils::{expression, matrix::MatrixIdUri, BoundObjectWeakRef, TemplateCallbacks},
38 Window,
39};
40
41mod imp {
42 use std::{
43 cell::{Cell, RefCell},
44 marker::PhantomData,
45 };
46
47 use glib::subclass::InitializingObject;
48
49 use super::*;
50
51 #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
52 #[template(
53 resource = "/org/gnome/Fractal/ui/session/view/content/room_details/general_page.ui"
54 )]
55 #[properties(wrapper_type = super::GeneralPage)]
56 pub struct GeneralPage {
57 #[template_child]
58 avatar: TemplateChild<Avatar>,
59 #[template_child]
60 room_topic: TemplateChild<gtk::Label>,
61 #[template_child]
62 edit_details_btn: TemplateChild<gtk::Button>,
63 #[template_child]
64 direct_members_group: TemplateChild<adw::PreferencesGroup>,
65 #[template_child]
66 direct_members_list: TemplateChild<gtk::ListBox>,
67 #[template_child]
68 no_direct_members_label: TemplateChild<gtk::Label>,
69 #[template_child]
70 members_row_group: TemplateChild<adw::PreferencesGroup>,
71 #[template_child]
72 members_row: TemplateChild<ButtonCountRow>,
73 #[template_child]
74 notifications: TemplateChild<adw::PreferencesGroup>,
75 #[template_child]
76 notifications_global_row: TemplateChild<CheckLoadingRow>,
77 #[template_child]
78 notifications_all_row: TemplateChild<CheckLoadingRow>,
79 #[template_child]
80 notifications_mentions_row: TemplateChild<CheckLoadingRow>,
81 #[template_child]
82 notifications_mute_row: TemplateChild<CheckLoadingRow>,
83 #[template_child]
84 addresses_group: TemplateChild<adw::PreferencesGroup>,
85 #[template_child]
86 edit_addresses_button: TemplateChild<gtk::Button>,
87 #[template_child]
88 no_addresses_label: TemplateChild<gtk::Label>,
89 canonical_alias_row: RefCell<Option<CopyableRow>>,
90 alt_aliases_rows: RefCell<Vec<CopyableRow>>,
91 #[template_child]
92 join_rule: TemplateChild<ComboLoadingRow>,
93 #[template_child]
94 guest_access: TemplateChild<SwitchLoadingRow>,
95 #[template_child]
96 publish: TemplateChild<SwitchLoadingRow>,
97 #[template_child]
98 history_visibility: TemplateChild<ComboLoadingRow>,
99 #[template_child]
100 encryption: TemplateChild<SwitchLoadingRow>,
101 #[template_child]
102 upgrade_button: TemplateChild<LoadingButton>,
103 #[template_child]
104 room_federated: TemplateChild<adw::ActionRow>,
105 #[property(get, set = Self::set_room, construct_only)]
107 room: BoundObjectWeakRef<Room>,
108 #[property(get, set = Self::set_membership_lists, construct_only)]
110 membership_lists: glib::WeakRef<MembershipLists>,
111 #[property(get = Self::notifications_setting, set = Self::set_notifications_setting, explicit_notify, builder(NotificationsRoomSetting::default()))]
113 notifications_setting: PhantomData<NotificationsRoomSetting>,
114 #[property(get)]
116 notifications_loading: Cell<bool>,
117 #[property(get)]
119 is_published: Cell<bool>,
120 expr_watch: RefCell<Option<gtk::ExpressionWatch>>,
121 notifications_settings_handlers: RefCell<Vec<glib::SignalHandlerId>>,
122 membership_handler: RefCell<Option<glib::SignalHandlerId>>,
123 permissions_handler: RefCell<Option<glib::SignalHandlerId>>,
124 canonical_alias_handler: RefCell<Option<glib::SignalHandlerId>>,
125 alt_aliases_handler: RefCell<Option<glib::SignalHandlerId>>,
126 join_rule_handler: RefCell<Option<glib::SignalHandlerId>>,
127 capabilities: RefCell<Capabilities>,
128 direct_members_list_has_bound_model: Cell<bool>,
129 }
130
131 #[glib::object_subclass]
132 impl ObjectSubclass for GeneralPage {
133 const NAME: &'static str = "RoomDetailsGeneralPage";
134 type Type = super::GeneralPage;
135 type ParentType = adw::PreferencesPage;
136
137 fn class_init(klass: &mut Self::Class) {
138 CopyableRow::ensure_type();
139
140 Self::bind_template(klass);
141 Self::bind_template_callbacks(klass);
142 TemplateCallbacks::bind_template_callbacks(klass);
143
144 klass
145 .install_property_action("room.set-notifications-setting", "notifications-setting");
146 }
147
148 fn instance_init(obj: &InitializingObject<Self>) {
149 obj.init_template();
150 }
151 }
152
153 #[glib::derived_properties]
154 impl ObjectImpl for GeneralPage {
155 fn constructed(&self) {
156 self.parent_constructed();
157 let obj = self.obj();
158
159 self.room_topic.connect_activate_link(clone!(
160 #[weak]
161 obj,
162 #[upgrade_or]
163 glib::Propagation::Proceed,
164 move |_, uri| {
165 let Ok(uri) = MatrixIdUri::parse(uri) else {
166 return glib::Propagation::Proceed;
167 };
168 let Some(room_details) = obj
169 .ancestor(RoomDetails::static_type())
170 .and_downcast::<RoomDetails>()
171 else {
172 return glib::Propagation::Proceed;
173 };
174 let Some(parent_window) = room_details.transient_for().and_downcast::<Window>()
175 else {
176 return glib::Propagation::Proceed;
177 };
178
179 parent_window.session_view().show_matrix_uri(uri);
180 room_details.close();
181
182 glib::Propagation::Stop
183 }
184 ));
185 }
186
187 fn dispose(&self) {
188 self.disconnect_all();
189 }
190 }
191
192 impl WidgetImpl for GeneralPage {}
193 impl PreferencesPageImpl for GeneralPage {}
194
195 #[gtk::template_callbacks]
196 impl GeneralPage {
197 #[allow(clippy::too_many_lines)]
199 fn set_room(&self, room: &Room) {
200 let obj = self.obj();
201
202 let membership_handler = room.own_member().connect_membership_notify(clone!(
203 #[weak(rename_to = imp)]
204 self,
205 move |_| {
206 imp.update_notifications();
207 }
208 ));
209 self.membership_handler.replace(Some(membership_handler));
210
211 let permissions_handler = room.permissions().connect_changed(clone!(
212 #[weak(rename_to = imp)]
213 self,
214 move |_| {
215 imp.update_upgrade_button();
216 imp.update_edit_addresses_button();
217 imp.update_join_rule();
218 imp.update_guest_access();
219 imp.update_history_visibility();
220 imp.update_encryption();
221
222 spawn!(async move {
223 imp.update_publish().await;
224 });
225 }
226 ));
227 self.permissions_handler.replace(Some(permissions_handler));
228
229 let aliases = room.aliases();
230 let canonical_alias_handler = aliases.connect_canonical_alias_string_notify(clone!(
231 #[weak(rename_to = imp)]
232 self,
233 move |_| {
234 imp.update_addresses();
235 }
236 ));
237 self.canonical_alias_handler
238 .replace(Some(canonical_alias_handler));
239
240 let alt_aliases_handler = aliases.alt_aliases_model().connect_items_changed(clone!(
241 #[weak(rename_to = imp)]
242 self,
243 move |_, _, _, _| {
244 imp.update_addresses();
245 }
246 ));
247 self.alt_aliases_handler.replace(Some(alt_aliases_handler));
248
249 let join_rule_handler = room.join_rule().connect_changed(clone!(
250 #[weak(rename_to = imp)]
251 self,
252 move |_| {
253 imp.update_join_rule();
254 }
255 ));
256 self.join_rule_handler.replace(Some(join_rule_handler));
257
258 let room_handler_ids = vec![
259 room.connect_joined_members_count_notify(clone!(
260 #[weak(rename_to = imp)]
261 self,
262 move |_| {
263 imp.update_members();
264 }
265 )),
266 room.connect_is_direct_notify(clone!(
267 #[weak(rename_to = imp)]
268 self,
269 move |_| {
270 imp.update_members();
271 }
272 )),
273 room.connect_category_notify(clone!(
274 #[weak(rename_to = imp)]
275 self,
276 move |_| {
277 imp.update_members();
278 }
279 )),
280 room.connect_notifications_setting_notify(clone!(
281 #[weak(rename_to = imp)]
282 self,
283 move |_| {
284 imp.update_notifications();
285 }
286 )),
287 room.connect_is_tombstoned_notify(clone!(
288 #[weak(rename_to = imp)]
289 self,
290 move |_| {
291 imp.update_upgrade_button();
292 }
293 )),
294 room.connect_guests_allowed_notify(clone!(
295 #[weak(rename_to = imp)]
296 self,
297 move |_| {
298 imp.update_guest_access();
299 }
300 )),
301 room.connect_history_visibility_notify(clone!(
302 #[weak(rename_to = imp)]
303 self,
304 move |_| {
305 imp.update_history_visibility();
306 }
307 )),
308 room.connect_is_encrypted_notify(clone!(
309 #[weak(rename_to = imp)]
310 self,
311 move |_| {
312 imp.update_encryption();
313 }
314 )),
315 ];
316
317 self.room.set(room, room_handler_ids);
318 obj.notify_room();
319
320 if let Some(session) = room.session() {
321 let notifications_settings = session.notifications().settings();
322 let notifications_settings_handlers = vec![
323 notifications_settings.connect_account_enabled_notify(clone!(
324 #[weak(rename_to = imp)]
325 self,
326 move |_| {
327 imp.update_notifications();
328 }
329 )),
330 notifications_settings.connect_session_enabled_notify(clone!(
331 #[weak(rename_to = imp)]
332 self,
333 move |_| {
334 imp.update_notifications();
335 }
336 )),
337 ];
338
339 self.notifications_settings_handlers
340 .replace(notifications_settings_handlers);
341 }
342
343 self.init_edit_details();
344 self.update_members();
345 self.update_notifications();
346 self.update_edit_addresses_button();
347 self.update_addresses();
348 self.update_federated();
349 self.update_join_rule();
350 self.update_guest_access();
351 self.update_publish_title();
352 self.update_history_visibility();
353 self.update_encryption();
354 self.update_upgrade_button();
355
356 spawn!(clone!(
357 #[weak(rename_to = imp)]
358 self,
359 async move {
360 imp.update_publish().await;
361 }
362 ));
363
364 self.load_capabilities();
365 }
366
367 fn set_membership_lists(&self, membership_lists: &MembershipLists) {
369 self.membership_lists.set(Some(membership_lists));
370 self.update_members();
371 }
372
373 fn notifications_setting(&self) -> NotificationsRoomSetting {
375 self.room
376 .obj()
377 .map(|r| r.notifications_setting())
378 .unwrap_or_default()
379 }
380
381 fn set_notifications_setting(&self, setting: NotificationsRoomSetting) {
383 if self.notifications_setting() == setting {
384 return;
385 }
386
387 self.notifications_setting_changed(setting);
388 }
389
390 fn load_capabilities(&self) {
392 let Some(room) = self.room.obj() else {
393 return;
394 };
395 let client = room.matrix_room().client();
396
397 spawn!(
398 glib::Priority::LOW,
399 clone!(
400 #[weak(rename_to = imp)]
401 self,
402 async move {
403 let handle = spawn_tokio!(async move { client.get_capabilities().await });
404 match handle.await.unwrap() {
405 Ok(capabilities) => {
406 imp.capabilities.replace(capabilities);
407 }
408 Err(error) => {
409 error!("Could not get server capabilities: {error}");
410 imp.capabilities.take();
411 }
412 }
413 }
414 )
415 );
416 }
417
418 fn init_edit_details(&self) {
420 let Some(room) = self.room.obj() else {
421 return;
422 };
423
424 let permissions = room.permissions();
427 let can_change_avatar = permissions.property_expression("can-change-avatar");
428 let can_change_name = permissions.property_expression("can-change-name");
429 let can_change_topic = permissions.property_expression("can-change-topic");
430
431 let can_change_name_or_topic = expression::or(can_change_name, can_change_topic);
432 let can_edit_at_least_one_detail =
433 expression::or(can_change_name_or_topic, can_change_avatar);
434
435 let is_direct_expr = room.property_expression("is-direct");
436
437 let expr_watch = expression::and(
438 expression::not(is_direct_expr),
439 can_edit_at_least_one_detail,
440 )
441 .bind(&*self.edit_details_btn, "visible", gtk::Widget::NONE);
442 self.expr_watch.replace(Some(expr_watch));
443 }
444
445 fn update_members(&self) {
447 let Some(room) = self.room.obj() else {
448 return;
449 };
450 let Some(membership_lists) = self.membership_lists.upgrade() else {
451 return;
452 };
453
454 let joined_members_count = membership_lists.joined().n_items();
455
456 let is_direct_with_few_members = room.is_direct() && joined_members_count < 5;
459 if is_direct_with_few_members {
460 let title = ngettext("Member", "Members", joined_members_count);
461 self.direct_members_group.set_title(&title);
462
463 if !self.direct_members_list_has_bound_model.get() {
466 self.direct_members_list
467 .bind_model(Some(&membership_lists.joined()), |item| {
468 let member = item
469 .downcast_ref::<Member>()
470 .expect("joined members list contains members");
471 let member_row = MemberRow::new(false);
472 member_row.set_member(Some(member));
473
474 gtk::ListBoxRow::builder()
475 .selectable(false)
476 .child(&member_row)
477 .action_name("details.show-member")
478 .action_target(&member.user_id().as_str().to_variant())
479 .build()
480 .upcast()
481 });
482 self.direct_members_list_has_bound_model.set(true);
483 }
484
485 let has_members = joined_members_count > 0;
486 self.direct_members_list.set_visible(has_members);
487 self.no_direct_members_label.set_visible(!has_members);
488 } else {
489 let mut server_joined_members_count = room.joined_members_count();
490
491 if room.category() == RoomCategory::Left {
492 server_joined_members_count = server_joined_members_count.saturating_sub(1);
496 }
497
498 let joined_members_count =
502 server_joined_members_count.max(joined_members_count.into());
503 self.members_row.set_count(joined_members_count.to_string());
504
505 let n = joined_members_count.try_into().unwrap_or(u32::MAX);
506 let title = ngettext("Member", "Members", n);
507 self.members_row.set_title(&title);
508
509 if self.direct_members_list_has_bound_model.get() {
510 self.direct_members_list
511 .bind_model(None::<&gio::ListModel>, |_item| {
512 gtk::ListBoxRow::new().upcast()
513 });
514 self.direct_members_list_has_bound_model.set(false);
515 }
516 }
517
518 self.direct_members_group
519 .set_visible(is_direct_with_few_members);
520 self.members_row_group
521 .set_visible(!is_direct_with_few_members);
522 }
523
524 fn disconnect_all(&self) {
526 if let Some(room) = self.room.obj() {
527 if let Some(session) = room.session() {
528 for handler in self.notifications_settings_handlers.take() {
529 session.notifications().settings().disconnect(handler);
530 }
531 }
532
533 if let Some(handler) = self.membership_handler.take() {
534 room.own_member().disconnect(handler);
535 }
536
537 if let Some(handler) = self.permissions_handler.take() {
538 room.permissions().disconnect(handler);
539 }
540
541 let aliases = room.aliases();
542 if let Some(handler) = self.canonical_alias_handler.take() {
543 aliases.disconnect(handler);
544 }
545 if let Some(handler) = self.alt_aliases_handler.take() {
546 aliases.alt_aliases_model().disconnect(handler);
547 }
548
549 if let Some(handler) = self.join_rule_handler.take() {
550 room.join_rule().disconnect(handler);
551 }
552 }
553
554 self.room.disconnect_signals();
555
556 if let Some(watch) = self.expr_watch.take() {
557 watch.unwatch();
558 }
559 }
560
561 fn update_notifications(&self) {
563 let Some(room) = self.room.obj() else {
564 return;
565 };
566
567 if !room.is_joined() {
568 self.notifications.set_visible(false);
569 return;
570 }
571
572 let Some(session) = room.session() else {
573 return;
574 };
575
576 self.obj().notify_notifications_setting();
578
579 let settings = session.notifications().settings();
580 let sensitive = settings.account_enabled()
581 && settings.session_enabled()
582 && !self.notifications_loading.get();
583 self.notifications.set_sensitive(sensitive);
584 self.notifications.set_visible(true);
585 }
586
587 fn set_notifications_loading(&self, loading: bool, setting: NotificationsRoomSetting) {
589 self.notifications_global_row
591 .set_is_loading(loading && setting == NotificationsRoomSetting::Global);
592 self.notifications_all_row
593 .set_is_loading(loading && setting == NotificationsRoomSetting::All);
594 self.notifications_mentions_row
595 .set_is_loading(loading && setting == NotificationsRoomSetting::MentionsOnly);
596 self.notifications_mute_row
597 .set_is_loading(loading && setting == NotificationsRoomSetting::Mute);
598
599 self.notifications_loading.set(loading);
600 self.obj().notify_notifications_loading();
601 }
602
603 fn notifications_setting_changed(&self, setting: NotificationsRoomSetting) {
605 let Some(room) = self.room.obj() else {
606 return;
607 };
608 let Some(session) = room.session() else {
609 return;
610 };
611
612 if setting == room.notifications_setting() {
613 return;
615 }
616
617 self.notifications.set_sensitive(false);
618 self.set_notifications_loading(true, setting);
619
620 let settings = session.notifications().settings();
621 spawn!(clone!(
622 #[weak(rename_to = imp)]
623 self,
624 async move {
625 if settings
626 .set_per_room_setting(room.room_id().to_owned(), setting)
627 .await
628 .is_err()
629 {
630 toast!(imp.obj(), gettext("Could not change notifications setting"));
631 }
632
633 imp.set_notifications_loading(false, setting);
634 imp.update_notifications();
635 }
636 ));
637 }
638
639 fn update_edit_addresses_button(&self) {
641 let Some(room) = self.room.obj() else {
642 return;
643 };
644
645 let can_edit = room.is_joined()
646 && room
647 .permissions()
648 .is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomPowerLevels));
649 self.edit_addresses_button.set_visible(can_edit);
650 }
651
652 fn update_addresses(&self) {
654 let Some(room) = self.room.obj() else {
655 return;
656 };
657 let aliases = room.aliases();
658
659 let canonical_alias_string = aliases.canonical_alias_string();
660 let has_canonical_alias = canonical_alias_string.is_some();
661
662 if let Some(canonical_alias_string) = canonical_alias_string {
663 let mut row_borrow = self.canonical_alias_row.borrow_mut();
664 let row = row_borrow.get_or_insert_with(|| {
665 self.remove_alt_aliases_rows();
668
669 let row = CopyableRow::new();
670 row.set_copy_button_tooltip_text(Some(gettext("Copy address")));
671 row.set_toast_text(Some(gettext("Address copied to clipboard")));
672
673 let label = gtk::Label::builder()
675 .label(gettext("Main Address"))
676 .ellipsize(pango::EllipsizeMode::End)
677 .css_classes(["public-address-tag"])
678 .valign(gtk::Align::Center)
679 .build();
680 row.update_relation(&[gtk::accessible::Relation::DescribedBy(&[
681 label.upcast_ref()
682 ])]);
683 row.set_extra_suffix(Some(label));
684
685 self.addresses_group.add(&row);
686
687 row
688 });
689
690 row.set_title(&canonical_alias_string);
691 } else if let Some(row) = self.canonical_alias_row.take() {
692 self.addresses_group.remove(&row);
693 }
694
695 let alt_aliases = aliases.alt_aliases_model();
696 let alt_aliases_count = alt_aliases.n_items() as usize;
697 if alt_aliases_count == 0 {
698 self.remove_alt_aliases_rows();
699 } else {
700 let mut rows = self.alt_aliases_rows.borrow_mut();
701
702 for (pos, alt_alias) in alt_aliases.iter::<glib::Object>().enumerate() {
703 let Some(alt_alias) = alt_alias.ok().and_downcast::<gtk::StringObject>() else {
704 break;
705 };
706
707 let row = rows.get(pos).cloned().unwrap_or_else(|| {
708 let row = CopyableRow::new();
709 row.set_copy_button_tooltip_text(Some(gettext("Copy address")));
710 row.set_toast_text(Some(gettext("Address copied to clipboard")));
711
712 self.addresses_group.add(&row);
713 rows.push(row.clone());
714
715 row
716 });
717
718 row.set_title(&alt_alias.string());
719 }
720
721 let rows_count = rows.len();
722 if alt_aliases_count < rows_count {
723 for _ in alt_aliases_count..rows_count {
724 if let Some(row) = rows.pop() {
725 self.addresses_group.remove(&row);
726 }
727 }
728 }
729 }
730
731 self.no_addresses_label
732 .set_visible(!has_canonical_alias && alt_aliases_count == 0);
733 }
734
735 fn remove_alt_aliases_rows(&self) {
736 for row in self.alt_aliases_rows.take() {
737 self.addresses_group.remove(&row);
738 }
739 }
740
741 #[template_callback]
743 async fn copy_permalink(&self) {
744 let Some(room) = self.room.obj() else {
745 return;
746 };
747
748 let permalink = room.matrix_to_uri().await;
749
750 let obj = self.obj();
751 obj.clipboard().set_text(&permalink.to_string());
752 toast!(obj, gettext("Room link copied to clipboard"));
753 }
754
755 fn update_join_rule(&self) {
757 let Some(room) = self.room.obj() else {
758 return;
759 };
760
761 let row = &self.join_rule;
762 row.set_is_loading(false);
763
764 let permissions = room.permissions();
765 let join_rule = room.join_rule();
766
767 let is_supported_join_rule = matches!(
768 join_rule.value(),
769 JoinRuleValue::Public | JoinRuleValue::Invite
770 ) && !join_rule.can_knock();
771 let can_change = permissions
772 .is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomJoinRules));
773
774 row.set_read_only(!is_supported_join_rule || !can_change);
775 row.set_selected_string(Some(join_rule.display_name()));
776 }
777
778 #[template_callback]
780 async fn set_join_rule(&self) {
781 let Some(room) = self.room.obj() else {
782 return;
783 };
784 let join_rule = room.join_rule();
785
786 let row = &self.join_rule;
787
788 let value = match row.selected() {
789 0 => JoinRuleValue::Invite,
790 1 => JoinRuleValue::Public,
791 _ => {
792 return;
793 }
794 };
795
796 if join_rule.value() == value {
797 return;
799 }
800
801 row.set_is_loading(true);
802 row.set_read_only(true);
803
804 if join_rule.set_value(value).await.is_err() {
805 toast!(self.obj(), gettext("Could not change who can join"));
806 self.update_join_rule();
807 }
808 }
809
810 fn update_guest_access(&self) {
812 let Some(room) = self.room.obj() else {
813 return;
814 };
815
816 let row = &self.guest_access;
817 row.set_is_active(room.guests_allowed());
818 row.set_is_loading(false);
819
820 let can_change = room
821 .permissions()
822 .is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomGuestAccess));
823 row.set_read_only(!can_change);
824 }
825
826 #[template_callback]
828 async fn toggle_guest_access(&self) {
829 let Some(room) = self.room.obj() else { return };
830
831 let row = &self.guest_access;
832 let guests_allowed = row.is_active();
833
834 if room.guests_allowed() == guests_allowed {
835 return;
836 }
837
838 row.set_is_loading(true);
839 row.set_read_only(true);
840
841 let guest_access = if guests_allowed {
842 GuestAccess::CanJoin
843 } else {
844 GuestAccess::Forbidden
845 };
846 let content = RoomGuestAccessEventContent::new(guest_access);
847
848 let matrix_room = room.matrix_room().clone();
849 let handle = spawn_tokio!(async move { matrix_room.send_state_event(content).await });
850
851 if let Err(error) = handle.await.unwrap() {
852 error!("Could not change guest access: {error}");
853 toast!(self.obj(), gettext("Could not change guest access"));
854 self.update_guest_access();
855 }
856 }
857
858 fn update_publish_title(&self) {
860 let Some(room) = self.room.obj() else {
861 return;
862 };
863
864 let own_member = room.own_member();
865 let server_name = own_member.user_id().server_name();
866
867 let title = gettext_f(
868 "Publish in the {homeserver} directory",
871 &[("homeserver", server_name.as_str())],
872 );
873 self.publish.set_title(&title);
874 }
875
876 async fn update_publish(&self) {
878 let Some(room) = self.room.obj() else {
879 return;
880 };
881
882 let row = &self.publish;
883
884 let can_change = room
888 .permissions()
889 .is_allowed_to(PowerLevelAction::SendState(
890 StateEventType::RoomCanonicalAlias,
891 ));
892 row.set_read_only(!can_change);
893
894 let matrix_room = room.matrix_room();
895 let client = matrix_room.client();
896 let request = get_room_visibility::v3::Request::new(matrix_room.room_id().to_owned());
897
898 let handle = spawn_tokio!(async move { client.send(request).await });
899
900 match handle.await.unwrap() {
901 Ok(response) => {
902 let is_published = response.visibility == Visibility::Public;
903 self.is_published.set(is_published);
904 row.set_is_active(is_published);
905 }
906 Err(error) => {
907 error!("Could not get directory visibility of room: {error}");
908 }
909 }
910
911 row.set_is_loading(false);
912 }
913
914 #[template_callback]
916 async fn toggle_publish(&self) {
917 let Some(room) = self.room.obj() else { return };
918
919 let row = &self.publish;
920 let publish = row.is_active();
921
922 if self.is_published.get() == publish {
923 return;
924 }
925
926 row.set_is_loading(true);
927 row.set_read_only(true);
928
929 let visibility = if publish {
930 Visibility::Public
931 } else {
932 Visibility::Private
933 };
934
935 let matrix_room = room.matrix_room();
936 let client = matrix_room.client();
937 let request =
938 set_room_visibility::v3::Request::new(matrix_room.room_id().to_owned(), visibility);
939
940 let handle = spawn_tokio!(async move { client.send(request).await });
941
942 if let Err(error) = handle.await.unwrap() {
943 error!("Could not change directory visibility of room: {error}");
944 let text = if publish {
945 gettext("Could not publish room in directory")
946 } else {
947 gettext("Could not unpublish room from directory")
948 };
949 toast!(self.obj(), text);
950 }
951
952 self.update_publish().await;
953 }
954
955 fn update_history_visibility(&self) {
957 let Some(room) = self.room.obj() else {
958 return;
959 };
960
961 let row = &self.history_visibility;
962 row.set_is_loading(false);
963
964 let visibility = room.history_visibility();
965
966 let text = match visibility {
967 HistoryVisibilityValue::WorldReadable => {
968 gettext("Anyone, even if they are not in the room")
969 }
970 HistoryVisibilityValue::Shared => {
971 gettext("Members only, since this option was selected")
972 }
973 HistoryVisibilityValue::Invited => gettext("Members only, since they were invited"),
974 HistoryVisibilityValue::Joined => {
975 gettext("Members only, since they joined the room")
976 }
977 HistoryVisibilityValue::Unsupported => gettext("Unsupported rule"),
978 };
979 row.set_selected_string(Some(text));
980
981 let is_supported = visibility != HistoryVisibilityValue::Unsupported;
982 let can_change = room
983 .permissions()
984 .is_allowed_to(PowerLevelAction::SendState(
985 StateEventType::RoomHistoryVisibility,
986 ));
987
988 row.set_read_only(!is_supported || !can_change);
989 }
990
991 #[template_callback]
993 async fn set_history_visibility(&self) {
994 let Some(room) = self.room.obj() else {
995 return;
996 };
997 let row = &self.history_visibility;
998
999 let visibility = match row.selected() {
1000 0 => HistoryVisibilityValue::WorldReadable,
1001 1 => HistoryVisibilityValue::Shared,
1002 2 => HistoryVisibilityValue::Joined,
1003 3 => HistoryVisibilityValue::Invited,
1004 _ => {
1005 return;
1006 }
1007 };
1008
1009 if room.history_visibility() == visibility {
1010 return;
1012 }
1013
1014 row.set_is_loading(true);
1015 row.set_read_only(true);
1016
1017 let content = RoomHistoryVisibilityEventContent::new(visibility.into());
1018
1019 let matrix_room = room.matrix_room().clone();
1020 let handle = spawn_tokio!(async move { matrix_room.send_state_event(content).await });
1021
1022 if let Err(error) = handle.await.unwrap() {
1023 error!("Could not change room history visibility: {error}");
1024 toast!(self.obj(), gettext("Could not change who can read history"));
1025
1026 self.update_history_visibility();
1027 }
1028 }
1029
1030 fn update_encryption(&self) {
1032 let Some(room) = self.room.obj() else {
1033 return;
1034 };
1035
1036 let row = &self.encryption;
1037 row.set_is_loading(false);
1038
1039 let is_encrypted = room.is_encrypted();
1040 row.set_is_active(is_encrypted);
1041
1042 let can_change = !is_encrypted
1043 && room
1044 .permissions()
1045 .is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomEncryption));
1046 row.set_read_only(!can_change);
1047 }
1048
1049 #[template_callback]
1051 async fn enable_encryption(&self) {
1052 let Some(room) = self.room.obj() else { return };
1053
1054 let row = &self.encryption;
1055
1056 if room.is_encrypted() || !row.is_active() {
1057 return;
1059 }
1060
1061 row.set_is_loading(true);
1062 row.set_read_only(true);
1063
1064 let dialog = adw::AlertDialog::builder()
1066 .heading(gettext("Enable Encryption?"))
1067 .body(gettext("Enabling encryption will prevent new members to read the history before they arrived. This cannot be disabled later."))
1068 .default_response("cancel")
1069 .build();
1070 dialog.add_responses(&[
1071 ("cancel", &gettext("Cancel")),
1072 ("enable", &gettext("Enable")),
1073 ]);
1074 dialog.set_response_appearance("enable", adw::ResponseAppearance::Destructive);
1075
1076 let obj = self.obj();
1077 if dialog.choose_future(&*obj).await != "enable" {
1078 self.update_encryption();
1079 return;
1080 }
1081
1082 if room.enable_encryption().await.is_err() {
1083 toast!(obj, gettext("Could not enable encryption"));
1084 self.update_encryption();
1085 }
1086 }
1087
1088 fn update_upgrade_button(&self) {
1090 let Some(room) = self.room.obj() else {
1091 return;
1092 };
1093
1094 let can_upgrade = !room.is_tombstoned()
1095 && room
1096 .permissions()
1097 .is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomTombstone));
1098 self.upgrade_button.set_visible(can_upgrade);
1099 }
1100
1101 fn update_federated(&self) {
1103 let Some(room) = self.room.obj() else {
1104 return;
1105 };
1106
1107 let subtitle = if room.federated() {
1108 gettext("Federated")
1110 } else {
1111 gettext("Not federated")
1113 };
1114
1115 self.room_federated.set_subtitle(&subtitle);
1116 }
1117
1118 #[template_callback]
1120 async fn upgrade(&self) {
1121 let Some(room) = self.room.obj() else {
1122 return;
1123 };
1124
1125 let obj = self.obj();
1126 self.upgrade_button.set_is_loading(true);
1128 let room_versions_capability = self.capabilities.borrow().room_versions.clone();
1129
1130 let Some(new_version) = confirm_room_upgrade(room_versions_capability, &*obj).await
1131 else {
1132 self.upgrade_button.set_is_loading(false);
1133 return;
1134 };
1135
1136 let client = room.matrix_room().client();
1137 let request = upgrade_room::v3::Request::new(room.room_id().to_owned(), new_version);
1138
1139 let handle = spawn_tokio!(async move { client.send(request).await });
1140
1141 match handle.await.unwrap() {
1142 Ok(_) => {
1143 toast!(obj, gettext("Room upgraded successfully"));
1144 }
1145 Err(error) => {
1146 error!("Could not upgrade room: {error}");
1147 toast!(obj, gettext("Could not upgrade room"));
1148 self.upgrade_button.set_is_loading(false);
1149 }
1150 }
1151 }
1152
1153 pub(super) fn unselect_topic(&self) {
1158 if self.room_topic.is_visible() {
1161 self.room_topic.select_region(0, 0);
1162 }
1163 }
1164 }
1165}
1166
1167glib::wrapper! {
1168 pub struct GeneralPage(ObjectSubclass<imp::GeneralPage>)
1170 @extends gtk::Widget, adw::PreferencesPage, @implements gtk::Accessible;
1171}
1172
1173impl GeneralPage {
1174 pub fn new(room: &Room, membership_lists: &MembershipLists) -> Self {
1175 glib::Object::builder()
1176 .property("room", room)
1177 .property("membership-lists", membership_lists)
1178 .build()
1179 }
1180
1181 pub(crate) fn unselect_topic(&self) {
1186 let imp = self.imp();
1187
1188 glib::idle_add_local_once(clone!(
1189 #[weak]
1190 imp,
1191 move || {
1192 imp.unselect_topic();
1193 }
1194 ));
1195 }
1196}