use adw::{prelude::*, subclass::prelude::*};
use gettextrs::{gettext, ngettext};
use gtk::{
gio,
glib::{self, clone},
pango, CompositeTemplate,
};
use ruma::{
api::client::{
directory::{get_room_visibility, set_room_visibility},
discovery::get_capabilities::Capabilities,
room::{upgrade_room, Visibility},
},
events::{
room::{
guest_access::{GuestAccess, RoomGuestAccessEventContent},
history_visibility::RoomHistoryVisibilityEventContent,
power_levels::PowerLevelAction,
},
StateEventType,
},
};
use tracing::error;
use super::{room_upgrade_dialog::confirm_room_upgrade, MemberRow, MembershipLists, RoomDetails};
use crate::{
components::{
ButtonCountRow, CheckLoadingRow, ComboLoadingRow, CopyableRow, LoadingButton,
SwitchLoadingRow,
},
gettext_f,
prelude::*,
session::model::{
HistoryVisibilityValue, JoinRuleValue, Member, NotificationsRoomSetting, Room, RoomCategory,
},
spawn, spawn_tokio, toast,
utils::{
expression, matrix::MatrixIdUri, template_callbacks::TemplateCallbacks, BoundObjectWeakRef,
OngoingAsyncAction,
},
Window,
};
mod imp {
use std::{
cell::{Cell, RefCell},
marker::PhantomData,
};
use glib::subclass::InitializingObject;
use super::*;
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(
resource = "/org/gnome/Fractal/ui/session/view/content/room_details/general_page.ui"
)]
#[properties(wrapper_type = super::GeneralPage)]
pub struct GeneralPage {
#[template_child]
pub room_topic: TemplateChild<gtk::Label>,
#[template_child]
pub edit_details_btn: TemplateChild<gtk::Button>,
#[template_child]
pub direct_members_group: TemplateChild<adw::PreferencesGroup>,
#[template_child]
pub direct_members_list: TemplateChild<gtk::ListBox>,
#[template_child]
pub no_direct_members_label: TemplateChild<gtk::Label>,
#[template_child]
pub members_row_group: TemplateChild<adw::PreferencesGroup>,
#[template_child]
pub members_row: TemplateChild<ButtonCountRow>,
#[template_child]
pub notifications: TemplateChild<adw::PreferencesGroup>,
#[template_child]
pub notifications_global_row: TemplateChild<CheckLoadingRow>,
#[template_child]
pub notifications_all_row: TemplateChild<CheckLoadingRow>,
#[template_child]
pub notifications_mentions_row: TemplateChild<CheckLoadingRow>,
#[template_child]
pub notifications_mute_row: TemplateChild<CheckLoadingRow>,
#[template_child]
pub addresses_group: TemplateChild<adw::PreferencesGroup>,
#[template_child]
pub edit_addresses_button: TemplateChild<gtk::Button>,
#[template_child]
pub no_addresses_label: TemplateChild<gtk::Label>,
pub canonical_alias_row: RefCell<Option<CopyableRow>>,
pub alt_aliases_rows: RefCell<Vec<CopyableRow>>,
#[template_child]
pub join_rule: TemplateChild<ComboLoadingRow>,
#[template_child]
pub guest_access: TemplateChild<SwitchLoadingRow>,
#[template_child]
pub publish: TemplateChild<SwitchLoadingRow>,
#[template_child]
pub history_visibility: TemplateChild<ComboLoadingRow>,
#[template_child]
pub encryption: TemplateChild<SwitchLoadingRow>,
#[template_child]
pub upgrade_button: TemplateChild<LoadingButton>,
#[template_child]
pub room_federated: TemplateChild<adw::ActionRow>,
#[property(get, set = Self::set_room, construct_only)]
room: BoundObjectWeakRef<Room>,
#[property(get, set = Self::set_membership_lists, construct_only)]
membership_lists: glib::WeakRef<MembershipLists>,
#[property(get = Self::notifications_setting, set = Self::set_notifications_setting, explicit_notify, builder(NotificationsRoomSetting::default()))]
pub notifications_setting: PhantomData<NotificationsRoomSetting>,
#[property(get)]
pub notifications_loading: Cell<bool>,
#[property(get)]
pub is_published: Cell<bool>,
pub changing_avatar: RefCell<Option<OngoingAsyncAction<String>>>,
pub changing_name: RefCell<Option<OngoingAsyncAction<String>>>,
pub changing_topic: RefCell<Option<OngoingAsyncAction<String>>>,
pub expr_watch: RefCell<Option<gtk::ExpressionWatch>>,
pub notifications_settings_handlers: RefCell<Vec<glib::SignalHandlerId>>,
pub membership_handler: RefCell<Option<glib::SignalHandlerId>>,
pub permissions_handler: RefCell<Option<glib::SignalHandlerId>>,
pub canonical_alias_handler: RefCell<Option<glib::SignalHandlerId>>,
pub alt_aliases_handler: RefCell<Option<glib::SignalHandlerId>>,
pub join_rule_handler: RefCell<Option<glib::SignalHandlerId>>,
pub capabilities: RefCell<Capabilities>,
direct_members_list_has_bound_model: Cell<bool>,
}
#[glib::object_subclass]
impl ObjectSubclass for GeneralPage {
const NAME: &'static str = "RoomDetailsGeneralPage";
type Type = super::GeneralPage;
type ParentType = adw::PreferencesPage;
fn class_init(klass: &mut Self::Class) {
CopyableRow::ensure_type();
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
TemplateCallbacks::bind_template_callbacks(klass);
klass
.install_property_action("room.set-notifications-setting", "notifications-setting");
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for GeneralPage {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
self.room_topic.connect_activate_link(clone!(
#[weak]
obj,
#[upgrade_or]
glib::Propagation::Proceed,
move |_, uri| {
let Ok(uri) = MatrixIdUri::parse(uri) else {
return glib::Propagation::Proceed;
};
let Some(room_details) = obj
.ancestor(RoomDetails::static_type())
.and_downcast::<RoomDetails>()
else {
return glib::Propagation::Proceed;
};
let Some(parent_window) = room_details.transient_for().and_downcast::<Window>()
else {
return glib::Propagation::Proceed;
};
parent_window.session_view().show_matrix_uri(uri);
room_details.close();
glib::Propagation::Stop
}
));
}
fn dispose(&self) {
self.disconnect_all();
}
}
impl WidgetImpl for GeneralPage {}
impl PreferencesPageImpl for GeneralPage {}
impl GeneralPage {
#[allow(clippy::too_many_lines)]
fn set_room(&self, room: &Room) {
let obj = self.obj();
let membership_handler = room.own_member().connect_membership_notify(clone!(
#[weak]
obj,
move |_| {
obj.update_notifications();
}
));
self.membership_handler.replace(Some(membership_handler));
let permissions_handler = room.permissions().connect_changed(clone!(
#[weak]
obj,
move |_| {
obj.update_upgrade_button();
obj.update_edit_addresses_button();
obj.update_join_rule();
obj.update_guest_access();
obj.update_history_visibility();
obj.update_encryption();
spawn!(async move {
obj.update_publish().await;
});
}
));
self.permissions_handler.replace(Some(permissions_handler));
let aliases = room.aliases();
let canonical_alias_handler = aliases.connect_canonical_alias_string_notify(clone!(
#[weak]
obj,
move |_| {
obj.update_addresses();
}
));
self.canonical_alias_handler
.replace(Some(canonical_alias_handler));
let alt_aliases_handler = aliases.alt_aliases_model().connect_items_changed(clone!(
#[weak]
obj,
move |_, _, _, _| {
obj.update_addresses();
}
));
self.alt_aliases_handler.replace(Some(alt_aliases_handler));
let join_rule_handler = room.join_rule().connect_changed(clone!(
#[weak]
obj,
move |_| {
obj.update_join_rule();
}
));
self.join_rule_handler.replace(Some(join_rule_handler));
let room_handler_ids = vec![
room.connect_joined_members_count_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update_members();
}
)),
room.connect_is_direct_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update_members();
}
)),
room.connect_category_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update_members();
}
)),
room.connect_notifications_setting_notify(clone!(
#[weak]
obj,
move |_| {
obj.update_notifications();
}
)),
room.connect_is_tombstoned_notify(clone!(
#[weak]
obj,
move |_| {
obj.update_upgrade_button();
}
)),
room.connect_guests_allowed_notify(clone!(
#[weak]
obj,
move |_| {
obj.update_guest_access();
}
)),
room.connect_history_visibility_notify(clone!(
#[weak]
obj,
move |_| {
obj.update_history_visibility();
}
)),
room.connect_is_encrypted_notify(clone!(
#[weak]
obj,
move |_| {
obj.update_encryption();
}
)),
];
self.room.set(room, room_handler_ids);
obj.notify_room();
if let Some(session) = room.session() {
let settings = session.notifications().settings();
let notifications_settings_handlers = vec![
settings.connect_account_enabled_notify(clone!(
#[weak]
obj,
move |_| {
obj.update_notifications();
}
)),
settings.connect_session_enabled_notify(clone!(
#[weak]
obj,
move |_| {
obj.update_notifications();
}
)),
];
self.notifications_settings_handlers
.replace(notifications_settings_handlers);
}
self.init_edit_details();
self.update_members();
obj.update_notifications();
obj.update_edit_addresses_button();
obj.update_addresses();
obj.update_federated();
obj.update_join_rule();
obj.update_guest_access();
obj.update_publish_title();
obj.update_history_visibility();
obj.update_encryption();
obj.update_upgrade_button();
spawn!(clone!(
#[weak]
obj,
async move {
obj.update_publish().await;
}
));
self.load_capabilities();
}
fn set_membership_lists(&self, membership_lists: &MembershipLists) {
self.membership_lists.set(Some(membership_lists));
self.update_members();
}
fn notifications_setting(&self) -> NotificationsRoomSetting {
self.room
.obj()
.map(|r| r.notifications_setting())
.unwrap_or_default()
}
fn set_notifications_setting(&self, setting: NotificationsRoomSetting) {
if self.notifications_setting() == setting {
return;
}
self.obj().notifications_setting_changed(setting);
}
fn load_capabilities(&self) {
let Some(room) = self.room.obj() else {
return;
};
let client = room.matrix_room().client();
spawn!(
glib::Priority::LOW,
clone!(
#[weak(rename_to = imp)]
self,
async move {
let handle = spawn_tokio!(async move { client.get_capabilities().await });
match handle.await.unwrap() {
Ok(capabilities) => {
imp.capabilities.replace(capabilities);
}
Err(error) => {
error!("Could not get server capabilities: {error}");
imp.capabilities.take();
}
}
}
)
);
}
fn init_edit_details(&self) {
let Some(room) = self.room.obj() else {
return;
};
let permissions = room.permissions();
let can_change_avatar = permissions.property_expression("can-change-avatar");
let can_change_name = permissions.property_expression("can-change-name");
let can_change_topic = permissions.property_expression("can-change-topic");
let can_change_name_or_topic = expression::or(can_change_name, can_change_topic);
let can_edit_at_least_one_detail =
expression::or(can_change_name_or_topic, can_change_avatar);
let is_direct_expr = room.property_expression("is-direct");
let expr_watch = expression::and(
expression::not(is_direct_expr),
can_edit_at_least_one_detail,
)
.bind(&*self.edit_details_btn, "visible", gtk::Widget::NONE);
self.expr_watch.replace(Some(expr_watch));
}
fn update_members(&self) {
let Some(room) = self.room.obj() else {
return;
};
let Some(membership_lists) = self.membership_lists.upgrade() else {
return;
};
let joined_members_count = membership_lists.joined().n_items();
let is_direct_with_few_members = room.is_direct() && joined_members_count < 5;
if is_direct_with_few_members {
let title = ngettext("Member", "Members", joined_members_count);
self.direct_members_group.set_title(&title);
if !self.direct_members_list_has_bound_model.get() {
self.direct_members_list
.bind_model(Some(&membership_lists.joined()), |item| {
let member = item
.downcast_ref::<Member>()
.expect("joined members list contains members");
let member_row = MemberRow::new(false);
member_row.set_member(Some(member));
gtk::ListBoxRow::builder()
.selectable(false)
.child(&member_row)
.action_name("details.show-member")
.action_target(&member.user_id().as_str().to_variant())
.build()
.upcast()
});
self.direct_members_list_has_bound_model.set(true);
}
let has_members = joined_members_count > 0;
self.direct_members_list.set_visible(has_members);
self.no_direct_members_label.set_visible(!has_members);
} else {
let mut server_joined_members_count = room.joined_members_count();
if room.category() == RoomCategory::Left {
server_joined_members_count = server_joined_members_count.saturating_sub(1);
}
let joined_members_count =
server_joined_members_count.max(joined_members_count.into());
self.members_row.set_count(joined_members_count.to_string());
let n = joined_members_count.try_into().unwrap_or(u32::MAX);
let title = ngettext("Member", "Members", n);
self.members_row.set_title(&title);
if self.direct_members_list_has_bound_model.get() {
self.direct_members_list
.bind_model(None::<&gio::ListModel>, |_item| {
gtk::ListBoxRow::new().upcast()
});
self.direct_members_list_has_bound_model.set(false);
}
}
self.direct_members_group
.set_visible(is_direct_with_few_members);
self.members_row_group
.set_visible(!is_direct_with_few_members);
}
fn disconnect_all(&self) {
if let Some(room) = self.room.obj() {
if let Some(session) = room.session() {
let settings = session.notifications().settings();
for handler in self.notifications_settings_handlers.take() {
settings.disconnect(handler);
}
}
if let Some(handler) = self.membership_handler.take() {
room.own_member().disconnect(handler);
}
if let Some(handler) = self.permissions_handler.take() {
room.permissions().disconnect(handler);
}
let aliases = room.aliases();
if let Some(handler) = self.canonical_alias_handler.take() {
aliases.disconnect(handler);
}
if let Some(handler) = self.alt_aliases_handler.take() {
aliases.alt_aliases_model().disconnect(handler);
}
if let Some(handler) = self.join_rule_handler.take() {
room.join_rule().disconnect(handler);
}
}
self.room.disconnect_signals();
if let Some(watch) = self.expr_watch.take() {
watch.unwatch();
}
}
}
}
glib::wrapper! {
pub struct GeneralPage(ObjectSubclass<imp::GeneralPage>)
@extends gtk::Widget, adw::PreferencesPage, @implements gtk::Accessible;
}
#[gtk::template_callbacks]
impl GeneralPage {
pub fn new(room: &Room, membership_lists: &MembershipLists) -> Self {
glib::Object::builder()
.property("room", room)
.property("membership-lists", membership_lists)
.build()
}
pub fn unselect_topic(&self) {
let imp = self.imp();
glib::idle_add_local_once(clone!(
#[weak]
imp,
move || {
if imp.room_topic.is_visible() {
imp.room_topic.select_region(0, 0);
}
}
));
}
fn update_notifications(&self) {
let Some(room) = self.room() else {
return;
};
let imp = self.imp();
if !room.is_joined() {
imp.notifications.set_visible(false);
return;
}
let Some(session) = room.session() else {
return;
};
self.notify_notifications_setting();
let settings = session.notifications().settings();
let sensitive = settings.account_enabled()
&& settings.session_enabled()
&& !self.notifications_loading();
imp.notifications.set_sensitive(sensitive);
imp.notifications.set_visible(true);
}
fn set_notifications_loading(&self, loading: bool, setting: NotificationsRoomSetting) {
let imp = self.imp();
imp.notifications_global_row
.set_is_loading(loading && setting == NotificationsRoomSetting::Global);
imp.notifications_all_row
.set_is_loading(loading && setting == NotificationsRoomSetting::All);
imp.notifications_mentions_row
.set_is_loading(loading && setting == NotificationsRoomSetting::MentionsOnly);
imp.notifications_mute_row
.set_is_loading(loading && setting == NotificationsRoomSetting::Mute);
self.imp().notifications_loading.set(loading);
self.notify_notifications_loading();
}
fn notifications_setting_changed(&self, setting: NotificationsRoomSetting) {
let Some(room) = self.room() else {
return;
};
let Some(session) = room.session() else {
return;
};
let imp = self.imp();
if setting == room.notifications_setting() {
return;
}
imp.notifications.set_sensitive(false);
self.set_notifications_loading(true, setting);
let settings = session.notifications().settings();
spawn!(clone!(
#[weak(rename_to = obj)]
self,
async move {
if settings
.set_per_room_setting(room.room_id().to_owned(), setting)
.await
.is_err()
{
toast!(obj, gettext("Could not change notifications setting"));
}
obj.set_notifications_loading(false, setting);
obj.update_notifications();
}
));
}
fn update_edit_addresses_button(&self) {
let Some(room) = self.room() else {
return;
};
let can_edit = room.is_joined()
&& room
.permissions()
.is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomPowerLevels));
self.imp().edit_addresses_button.set_visible(can_edit);
}
fn update_addresses(&self) {
let Some(room) = self.room() else {
return;
};
let imp = self.imp();
let aliases = room.aliases();
let canonical_alias_string = aliases.canonical_alias_string();
let has_canonical_alias = canonical_alias_string.is_some();
if let Some(canonical_alias_string) = canonical_alias_string {
let mut row_borrow = imp.canonical_alias_row.borrow_mut();
let row = row_borrow.get_or_insert_with(|| {
self.remove_alt_aliases_rows();
let row = CopyableRow::new();
row.set_copy_button_tooltip_text(Some(gettext("Copy address")));
row.set_toast_text(Some(gettext("Address copied to clipboard")));
let label = gtk::Label::builder()
.label(gettext("Main Address"))
.ellipsize(pango::EllipsizeMode::End)
.css_classes(["public-address-tag"])
.valign(gtk::Align::Center)
.build();
row.update_relation(&[gtk::accessible::Relation::DescribedBy(&[
label.upcast_ref()
])]);
row.set_extra_suffix(Some(label));
imp.addresses_group.add(&row);
row
});
row.set_title(&canonical_alias_string);
} else if let Some(row) = imp.canonical_alias_row.take() {
imp.addresses_group.remove(&row);
}
let alt_aliases = aliases.alt_aliases_model();
let alt_aliases_count = alt_aliases.n_items() as usize;
if alt_aliases_count == 0 {
self.remove_alt_aliases_rows();
} else {
let mut rows = imp.alt_aliases_rows.borrow_mut();
for (pos, alt_alias) in alt_aliases.iter::<glib::Object>().enumerate() {
let Some(alt_alias) = alt_alias.ok().and_downcast::<gtk::StringObject>() else {
break;
};
let row = rows.get(pos).cloned().unwrap_or_else(|| {
let row = CopyableRow::new();
row.set_copy_button_tooltip_text(Some(gettext("Copy address")));
row.set_toast_text(Some(gettext("Address copied to clipboard")));
imp.addresses_group.add(&row);
rows.push(row.clone());
row
});
row.set_title(&alt_alias.string());
}
let rows_count = rows.len();
if alt_aliases_count < rows_count {
for _ in alt_aliases_count..rows_count {
if let Some(row) = rows.pop() {
imp.addresses_group.remove(&row);
}
}
}
}
imp.no_addresses_label
.set_visible(!has_canonical_alias && alt_aliases_count == 0);
}
fn remove_alt_aliases_rows(&self) {
let imp = self.imp();
for row in imp.alt_aliases_rows.take() {
imp.addresses_group.remove(&row);
}
}
#[template_callback]
async fn copy_permalink(&self) {
let Some(room) = self.room() else {
return;
};
let permalink = room.matrix_to_uri().await;
self.clipboard().set_text(&permalink.to_string());
toast!(self, gettext("Room link copied to clipboard"));
}
fn update_join_rule(&self) {
let Some(room) = self.room() else {
return;
};
let row = &self.imp().join_rule;
row.set_is_loading(false);
let permissions = room.permissions();
let join_rule = room.join_rule();
let is_supported_join_rule = matches!(
join_rule.value(),
JoinRuleValue::Public | JoinRuleValue::Invite
) && !join_rule.can_knock();
let can_change =
permissions.is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomJoinRules));
row.set_read_only(!is_supported_join_rule || !can_change);
row.set_selected_string(Some(join_rule.display_name()));
}
#[template_callback]
async fn set_join_rule(&self) {
let Some(room) = self.room() else {
return;
};
let join_rule = room.join_rule();
let row = &self.imp().join_rule;
let value = match row.selected() {
0 => JoinRuleValue::Invite,
1 => JoinRuleValue::Public,
_ => {
return;
}
};
if join_rule.value() == value {
return;
}
row.set_is_loading(true);
row.set_read_only(true);
if join_rule.set_value(value).await.is_err() {
toast!(self, gettext("Could not change who can join"));
self.update_join_rule();
}
}
fn update_guest_access(&self) {
let Some(room) = self.room() else {
return;
};
let row = &self.imp().guest_access;
row.set_is_active(room.guests_allowed());
row.set_is_loading(false);
let can_change = room
.permissions()
.is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomGuestAccess));
row.set_read_only(!can_change);
}
#[template_callback]
async fn toggle_guest_access(&self) {
let Some(room) = self.room() else { return };
let row = &self.imp().guest_access;
let guests_allowed = row.is_active();
if room.guests_allowed() == guests_allowed {
return;
}
row.set_is_loading(true);
row.set_read_only(true);
let guest_access = if guests_allowed {
GuestAccess::CanJoin
} else {
GuestAccess::Forbidden
};
let content = RoomGuestAccessEventContent::new(guest_access);
let matrix_room = room.matrix_room().clone();
let handle = spawn_tokio!(async move { matrix_room.send_state_event(content).await });
if let Err(error) = handle.await.unwrap() {
error!("Could not change guest access: {error}");
toast!(self, gettext("Could not change guest access"));
self.update_guest_access();
}
}
fn update_publish_title(&self) {
let Some(room) = self.room() else {
return;
};
let own_member = room.own_member();
let server_name = own_member.user_id().server_name();
let title = gettext_f(
"Publish in the {homeserver} directory",
&[("homeserver", server_name.as_str())],
);
self.imp().publish.set_title(&title);
}
async fn update_publish(&self) {
let Some(room) = self.room() else {
return;
};
let imp = self.imp();
let row = &imp.publish;
let can_change = room
.permissions()
.is_allowed_to(PowerLevelAction::SendState(
StateEventType::RoomCanonicalAlias,
));
row.set_read_only(!can_change);
let matrix_room = room.matrix_room();
let client = matrix_room.client();
let request = get_room_visibility::v3::Request::new(matrix_room.room_id().to_owned());
let handle = spawn_tokio!(async move { client.send(request).await });
match handle.await.unwrap() {
Ok(response) => {
let is_published = response.visibility == Visibility::Public;
imp.is_published.set(is_published);
row.set_is_active(is_published);
}
Err(error) => {
error!("Could not get directory visibility of room: {error}");
}
}
row.set_is_loading(false);
}
#[template_callback]
async fn toggle_publish(&self) {
let Some(room) = self.room() else { return };
let imp = self.imp();
let row = &imp.publish;
let publish = row.is_active();
if imp.is_published.get() == publish {
return;
}
row.set_is_loading(true);
row.set_read_only(true);
let visibility = if publish {
Visibility::Public
} else {
Visibility::Private
};
let matrix_room = room.matrix_room();
let client = matrix_room.client();
let request =
set_room_visibility::v3::Request::new(matrix_room.room_id().to_owned(), visibility);
let handle = spawn_tokio!(async move { client.send(request).await });
if let Err(error) = handle.await.unwrap() {
error!("Could not change directory visibility of room: {error}");
let text = if publish {
gettext("Could not publish room in directory")
} else {
gettext("Could not unpublish room from directory")
};
toast!(self, text);
}
self.update_publish().await;
}
fn update_history_visibility(&self) {
let Some(room) = self.room() else {
return;
};
let row = &self.imp().history_visibility;
row.set_is_loading(false);
let visibility = room.history_visibility();
let text = match visibility {
HistoryVisibilityValue::WorldReadable => {
gettext("Anyone, even if they are not in the room")
}
HistoryVisibilityValue::Shared => {
gettext("Members only, since this option was selected")
}
HistoryVisibilityValue::Invited => gettext("Members only, since they were invited"),
HistoryVisibilityValue::Joined => gettext("Members only, since they joined the room"),
HistoryVisibilityValue::Unsupported => gettext("Unsupported rule"),
};
row.set_selected_string(Some(text));
let is_supported = visibility != HistoryVisibilityValue::Unsupported;
let can_change = room
.permissions()
.is_allowed_to(PowerLevelAction::SendState(
StateEventType::RoomHistoryVisibility,
));
row.set_read_only(!is_supported || !can_change);
}
#[template_callback]
async fn set_history_visibility(&self) {
let Some(room) = self.room() else {
return;
};
let row = &self.imp().history_visibility;
let visibility = match row.selected() {
0 => HistoryVisibilityValue::WorldReadable,
1 => HistoryVisibilityValue::Shared,
2 => HistoryVisibilityValue::Joined,
3 => HistoryVisibilityValue::Invited,
_ => {
return;
}
};
if room.history_visibility() == visibility {
return;
}
row.set_is_loading(true);
row.set_read_only(true);
let content = RoomHistoryVisibilityEventContent::new(visibility.into());
let matrix_room = room.matrix_room().clone();
let handle = spawn_tokio!(async move { matrix_room.send_state_event(content).await });
if let Err(error) = handle.await.unwrap() {
error!("Could not change room history visibility: {error}");
toast!(self, gettext("Could not change who can read history"));
self.update_history_visibility();
}
}
fn update_encryption(&self) {
let Some(room) = self.room() else {
return;
};
let imp = self.imp();
let row = &imp.encryption;
row.set_is_loading(false);
let is_encrypted = room.is_encrypted();
row.set_is_active(is_encrypted);
let can_change = !is_encrypted
&& room
.permissions()
.is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomEncryption));
row.set_read_only(!can_change);
}
#[template_callback]
async fn enable_encryption(&self) {
let Some(room) = self.room() else { return };
let imp = self.imp();
let row = &imp.encryption;
if room.is_encrypted() || !row.is_active() {
return;
}
row.set_is_loading(true);
row.set_read_only(true);
let dialog = adw::AlertDialog::builder()
.heading(gettext("Enable Encryption?"))
.body(gettext("Enabling encryption will prevent new members to read the history before they arrived. This cannot be disabled later."))
.default_response("cancel")
.build();
dialog.add_responses(&[
("cancel", &gettext("Cancel")),
("enable", &gettext("Enable")),
]);
dialog.set_response_appearance("enable", adw::ResponseAppearance::Destructive);
if dialog.choose_future(self).await != "enable" {
self.update_encryption();
return;
};
if room.enable_encryption().await.is_err() {
toast!(self, gettext("Could not enable encryption"));
self.update_encryption();
}
}
fn update_upgrade_button(&self) {
let Some(room) = self.room() else {
return;
};
let can_upgrade = !room.is_tombstoned()
&& room
.permissions()
.is_allowed_to(PowerLevelAction::SendState(StateEventType::RoomTombstone));
self.imp().upgrade_button.set_visible(can_upgrade);
}
fn update_federated(&self) {
let Some(room) = self.room() else {
return;
};
let subtitle = if room.federated() {
gettext("Federated")
} else {
gettext("Not federated")
};
self.imp().room_federated.set_subtitle(&subtitle);
}
#[template_callback]
async fn upgrade(&self) {
let Some(room) = self.room() else {
return;
};
let imp = self.imp();
imp.upgrade_button.set_is_loading(true);
let room_versions_capability = imp.capabilities.borrow().room_versions.clone();
let Some(new_version) = confirm_room_upgrade(room_versions_capability, self).await else {
imp.upgrade_button.set_is_loading(false);
return;
};
let client = room.matrix_room().client();
let request = upgrade_room::v3::Request::new(room.room_id().to_owned(), new_version);
let handle = spawn_tokio!(async move { client.send(request).await });
match handle.await.unwrap() {
Ok(_) => {
toast!(self, gettext("Room upgraded successfully"));
}
Err(error) => {
error!("Could not upgrade room: {error}");
toast!(self, gettext("Could not upgrade room"));
imp.upgrade_button.set_is_loading(false);
}
}
}
}