room-details: Add notifications setting

This commit is contained in:
Kévin Commaille 2023-12-19 16:12:13 +01:00
parent f42688b225
commit db339a476a
No known key found for this signature in database
GPG Key ID: 29A48C1F03620416
6 changed files with 579 additions and 196 deletions

View File

@ -10,7 +10,9 @@ mod verification;
pub use self::{
avatar_data::{AvatarData, AvatarImage, AvatarUriSource},
notifications::{Notifications, NotificationsGlobalSetting, NotificationsSettings},
notifications::{
Notifications, NotificationsGlobalSetting, NotificationsRoomSetting, NotificationsSettings,
},
room::{
Event, EventKey, HighlightFlags, Member, MemberList, MemberRole, Membership, MessageState,
PowerLevel, ReactionGroup, ReactionList, Room, RoomType, Timeline, TimelineItem,

View File

@ -12,7 +12,9 @@ use tracing::{debug, error, warn};
mod notifications_settings;
pub use self::notifications_settings::{NotificationsGlobalSetting, NotificationsSettings};
pub use self::notifications_settings::{
NotificationsGlobalSetting, NotificationsRoomSetting, NotificationsSettings,
};
use super::{Room, Session};
use crate::{
application::AppShowRoomPayload, prelude::*, spawn, spawn_tokio, utils::matrix::get_event_body,

View File

@ -1,3 +1,5 @@
use std::collections::HashMap;
use futures_util::StreamExt;
use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*};
use matrix_sdk::{
@ -6,12 +8,15 @@ use matrix_sdk::{
},
NotificationSettingsError,
};
use ruma::push::{PredefinedOverrideRuleId, RuleKind};
use ruma::{
push::{PredefinedOverrideRuleId, RuleKind},
OwnedRoomId, RoomId,
};
use tokio::sync::broadcast::error::RecvError;
use tracing::{error, warn};
use crate::{
session::model::{Session, SessionState},
session::model::{Room, Session, SessionState},
spawn, spawn_tokio,
};
@ -32,6 +37,47 @@ pub enum NotificationsGlobalSetting {
MentionsOnly = 2,
}
/// The possible values for a room notifications setting.
#[derive(
Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum, strum::Display, strum::EnumString,
)]
#[repr(u32)]
#[enum_type(name = "NotificationsRoomSetting")]
#[strum(serialize_all = "kebab-case")]
pub enum NotificationsRoomSetting {
/// Use the global setting.
#[default]
Global = 0,
/// All messages.
All = 1,
/// Only mentions and keywords.
MentionsOnly = 2,
/// No notifications.
Mute = 3,
}
impl NotificationsRoomSetting {
/// Convert to a [`RoomNotificationMode`].
fn to_notification_mode(self) -> Option<RoomNotificationMode> {
match self {
Self::Global => None,
Self::All => Some(RoomNotificationMode::AllMessages),
Self::MentionsOnly => Some(RoomNotificationMode::MentionsAndKeywordsOnly),
Self::Mute => Some(RoomNotificationMode::Mute),
}
}
}
impl From<RoomNotificationMode> for NotificationsRoomSetting {
fn from(value: RoomNotificationMode) -> Self {
match value {
RoomNotificationMode::AllMessages => Self::All,
RoomNotificationMode::MentionsAndKeywordsOnly => Self::MentionsOnly,
RoomNotificationMode::Mute => Self::Mute,
}
}
}
mod imp {
use std::cell::{Cell, RefCell};
@ -57,6 +103,10 @@ mod imp {
/// The list of keywords that trigger notifications.
#[property(get)]
pub keywords_list: gtk::StringList,
/// The map of room ID to per-room notification setting.
///
/// Any room not in this map uses the global setting.
pub per_room_settings: RefCell<HashMap<OwnedRoomId, NotificationsRoomSetting>>,
}
#[glib::object_subclass]
@ -216,6 +266,7 @@ impl NotificationsSettings {
}
self.update_keywords_list().await;
self.update_per_room_settings().await;
}
/// Set whether notifications are enabled for this session.
@ -383,6 +434,95 @@ impl NotificationsSettings {
Ok(())
}
/// Update the local list of per-room settings with the remote one.
async fn update_per_room_settings(&self) {
let Some(api) = self.api() else {
return;
};
let api_clone = api.clone();
let room_ids = spawn_tokio!(async move {
api_clone
.get_rooms_with_user_defined_rules(Some(true))
.await
})
.await
.unwrap();
// Update the local map.
let mut per_room_settings = HashMap::with_capacity(room_ids.len());
for room_id in room_ids {
let Ok(room_id) = RoomId::parse(room_id) else {
continue;
};
let room_id_clone = room_id.clone();
let api_clone = api.clone();
let handle = spawn_tokio!(async move {
api_clone
.get_user_defined_room_notification_mode(&room_id_clone)
.await
});
if let Some(setting) = handle.await.unwrap() {
per_room_settings.insert(room_id, setting.into());
}
}
self.imp()
.per_room_settings
.replace(per_room_settings.clone());
// Update the setting in the rooms.
// Since we don't know when a room was added or removed, we have to update every
// room.
let Some(session) = self.session() else {
return;
};
let room_list = session.room_list();
for room in room_list.iter::<Room>() {
let Ok(room) = room else {
// Returns an error when the list changed, just stop.
break;
};
if let Some(setting) = per_room_settings.get(room.room_id()) {
room.set_notifications_setting(*setting);
} else {
room.set_notifications_setting(NotificationsRoomSetting::Global);
}
}
}
/// Set the notification setting for the room with the given ID.
pub async fn set_per_room_setting(
&self,
room_id: OwnedRoomId,
setting: NotificationsRoomSetting,
) -> Result<(), NotificationSettingsError> {
let Some(api) = self.api() else {
error!("Cannot update notifications settings when API is not initialized");
return Err(NotificationSettingsError::UnableToUpdatePushRule);
};
let room_id_clone = room_id.clone();
let handle = if let Some(mode) = setting.to_notification_mode() {
spawn_tokio!(async move { api.set_room_notification_mode(&room_id_clone, mode).await })
} else {
spawn_tokio!(async move { api.delete_user_defined_room_rules(&room_id_clone).await })
};
if let Err(error) = handle.await.unwrap() {
error!("Failed to update notifications setting for room `{room_id}`: {error}");
return Err(error);
}
self.update_per_room_settings().await;
Ok(())
}
}
impl Default for NotificationsSettings {

View File

@ -50,8 +50,8 @@ pub use self::{
typing_list::TypingList,
};
use super::{
room_list::RoomMetainfo, AvatarData, AvatarImage, AvatarUriSource, IdentityVerification,
Session, SidebarItem, SidebarItemImpl,
notifications::NotificationsRoomSetting, room_list::RoomMetainfo, AvatarData, AvatarImage,
AvatarUriSource, IdentityVerification, Session, SidebarItem, SidebarItemImpl,
};
use crate::{components::Pill, gettext_f, prelude::*, spawn, spawn_tokio};
@ -153,6 +153,9 @@ mod imp {
/// Whether this room has been upgraded.
#[property(get = Self::is_tombstoned)]
pub is_tombstoned: PhantomData<bool>,
/// The notifications settings for this room.
#[property(get, set = Self::set_notifications_setting, explicit_notify, builder(NotificationsRoomSetting::default()))]
pub notifications_setting: Cell<NotificationsRoomSetting>,
}
#[glib::object_subclass]
@ -239,7 +242,7 @@ mod imp {
}
/// The display name of this room.
pub fn display_name(&self) -> String {
fn display_name(&self) -> String {
let display_name = self.display_name.borrow().clone();
// Translators: This is displayed when the room name is unknown yet.
display_name.unwrap_or_else(|| gettext("Unknown"))
@ -268,9 +271,19 @@ mod imp {
}
/// Whether this room was tombstoned.
pub fn is_tombstoned(&self) -> bool {
fn is_tombstoned(&self) -> bool {
self.matrix_room.borrow().as_ref().unwrap().is_tombstoned()
}
/// Set the notifications setting for this room.
fn set_notifications_setting(&self, setting: NotificationsRoomSetting) {
if self.notifications_setting.get() == setting {
return;
}
self.notifications_setting.set(setting);
self.obj().notify_notifications_setting();
}
}
}

View File

@ -18,8 +18,8 @@ use ruma::{
use tracing::error;
use crate::{
components::{CustomEntry, EditableAvatar, SpinnerButton},
session::model::{AvatarData, AvatarImage, MemberList, Room},
components::{CustomEntry, EditableAvatar, LoadingBin, SpinnerButton},
session::model::{AvatarData, AvatarImage, MemberList, NotificationsRoomSetting, Room},
spawn, spawn_tokio, toast,
utils::{
and_expr,
@ -31,7 +31,10 @@ use crate::{
};
mod imp {
use std::cell::{Cell, RefCell};
use std::{
cell::{Cell, RefCell},
marker::PhantomData,
};
use glib::subclass::InitializingObject;
@ -63,13 +66,38 @@ mod imp {
pub save_details_btn: TemplateChild<SpinnerButton>,
#[template_child]
pub members_count: TemplateChild<gtk::Label>,
#[template_child]
pub notifications: TemplateChild<adw::PreferencesGroup>,
#[template_child]
pub notifications_global_bin: TemplateChild<LoadingBin>,
#[template_child]
pub notifications_global_radio: TemplateChild<gtk::CheckButton>,
#[template_child]
pub notifications_all_bin: TemplateChild<LoadingBin>,
#[template_child]
pub notifications_all_radio: TemplateChild<gtk::CheckButton>,
#[template_child]
pub notifications_mentions_bin: TemplateChild<LoadingBin>,
#[template_child]
pub notifications_mentions_radio: TemplateChild<gtk::CheckButton>,
#[template_child]
pub notifications_mute_bin: TemplateChild<LoadingBin>,
#[template_child]
pub notifications_mute_radio: TemplateChild<gtk::CheckButton>,
/// Whether edit mode is enabled.
#[property(get, set = Self::set_edit_mode_enabled, explicit_notify)]
pub edit_mode_enabled: Cell<bool>,
/// The notifications setting for the room.
#[property(get = Self::notifications_setting, set = Self::set_notifications_setting, explicit_notify, builder(NotificationsRoomSetting::default()))]
pub notifications_setting: PhantomData<NotificationsRoomSetting>,
/// Whether the notifications section is busy.
#[property(get)]
pub notifications_loading: 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_watches: RefCell<Vec<gtk::ExpressionWatch>>,
pub notifications_settings_handlers: RefCell<Vec<glib::SignalHandlerId>>,
}
#[glib::object_subclass]
@ -82,6 +110,9 @@ mod imp {
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>) {
@ -131,6 +162,9 @@ mod imp {
room.connect_joined_members_count_notify(clone!(@weak obj => move |room| {
obj.member_count_changed(room.joined_members_count());
})),
room.connect_notifications_setting_notify(clone!(@weak obj => move |_| {
obj.update_notifications();
})),
];
obj.member_count_changed(room.joined_members_count());
@ -143,6 +177,23 @@ mod imp {
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);
}
obj.update_notifications();
}
/// Set whether edit mode is enabled.
@ -156,6 +207,23 @@ mod imp {
self.edit_mode_enabled.set(enabled);
obj.notify_edit_mode_enabled();
}
/// The notifications setting for the room.
fn notifications_setting(&self) -> NotificationsRoomSetting {
self.room
.obj()
.map(|r| r.notifications_setting())
.unwrap_or_default()
}
/// Set the notifications setting for the room.
fn set_notifications_setting(&self, setting: NotificationsRoomSetting) {
if self.notifications_setting() == setting {
return;
}
self.obj().notifications_setting_changed(setting);
}
}
}
@ -564,10 +632,94 @@ impl GeneralPage {
fn disconnect_all(&self) {
let imp = self.imp();
if let Some(settings) = imp
.room
.obj()
.and_then(|r| r.session())
.map(|s| s.notifications().settings())
{
for handler in imp.notifications_settings_handlers.take() {
settings.disconnect(handler);
}
}
imp.room.disconnect_signals();
for watch in imp.expr_watches.take() {
watch.unwatch();
}
}
/// Update the section about notifications.
fn update_notifications(&self) {
let Some(room) = self.room() else {
return;
};
let Some(session) = room.session() else {
return;
};
let imp = self.imp();
// Updates the active radio button.
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);
}
/// Update the loading state in the notifications section.
fn set_notifications_loading(&self, loading: bool, setting: NotificationsRoomSetting) {
let imp = self.imp();
// Only show the spinner on the selected one.
imp.notifications_global_bin
.set_is_loading(loading && setting == NotificationsRoomSetting::Global);
imp.notifications_all_bin
.set_is_loading(loading && setting == NotificationsRoomSetting::All);
imp.notifications_mentions_bin
.set_is_loading(loading && setting == NotificationsRoomSetting::MentionsOnly);
imp.notifications_mute_bin
.set_is_loading(loading && setting == NotificationsRoomSetting::Mute);
self.imp().notifications_loading.set(loading);
self.notify_notifications_loading();
}
/// Handle a change of the notifications setting.
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() {
// Nothing to do.
return;
}
imp.notifications.set_sensitive(false);
self.set_notifications_loading(true, setting);
let settings = session.notifications().settings();
spawn!(
clone!(@weak self as obj, @weak room, @weak settings => 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();
})
);
}
}

View File

@ -2,190 +2,264 @@
<interface>
<template class="ContentRoomDetailsGeneralPage" parent="AdwPreferencesPage">
<property name="title" translatable="yes">Room Details</property>
<child>
<object class="AdwPreferencesGroup">
<style>
<class name="room-details-group"/>
</style>
<child>
<object class="ComponentsEditableAvatar" id="avatar">
<binding name="data">
<lookup name="avatar-data">
<lookup name="room">ContentRoomDetailsGeneralPage</lookup>
</lookup>
</binding>
</object>
</child>
<child>
<object class="GtkBox">
<property name="spacing">6</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkEntry" id="room_name_entry">
<property name="sensitive">false</property>
<property name="activates-default">True</property>
<property name="xalign">0.5</property>
<property name="buffer">
<object class="GtkEntryBuffer" id="room_name_buffer">
<binding name="text">
<lookup name="display-name">
<lookup name="room">ContentRoomDetailsGeneralPage</lookup>
</lookup>
</binding>
</object>
</property>
<style>
<class name="room-details-name"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel" id="room_topic_label">
<property name="visible">false</property>
<property name="margin-top">12</property>
<property name="label" translatable="yes">Description</property>
<property name="halign">start</property>
<style>
<class name="dim-label"/>
<class name="caption-heading"/>
</style>
</object>
</child>
<child>
<object class="CustomEntry" id="room_topic_entry">
<property name="sensitive">false</property>
<property name="margin-bottom">18</property>
<child>
<object class="GtkTextView" id="room_topic_text_view">
<property name="justification">center</property>
<property name="wrap-mode">word-char</property>
<property name="accepts-tab">False</property>
<property name="top-margin">7</property>
<property name="bottom-margin">7</property>
<property name="buffer">
<object class="GtkTextBuffer" id="room_topic_buffer">
<binding name="text">
<closure type="gchararray" function="unwrap_string_or_empty">
<lookup name="topic">
<lookup name="room">ContentRoomDetailsGeneralPage</lookup>
</lookup>
</closure>
</binding>
</object>
</property>
</object>
</child>
<style>
<class name="room-details-topic"/>
</style>
</object>
</child>
<child>
<object class="GtkButton" id="edit_details_btn">
<property name="can-shrink">true</property>
<property name="halign">center</property>
<property name="label" translatable="yes">Edit Details</property>
<signal name="clicked" handler="edit_details_clicked" swapped="yes"/>
<style>
<class name="pill" />
</style>
</object>
</child>
<child>
<object class="SpinnerButton" id="save_details_btn">
<property name="visible" bind-source="ContentRoomDetailsGeneralPage" bind-property="edit-mode-enabled" bind-flags="sync-create"/>
<property name="halign">center</property>
<property name="label" translatable="yes">Save Details</property>
<signal name="clicked" handler="save_details_clicked" swapped="yes"/>
<style>
<class name="pill" />
</style>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">Members</property>
<property name="icon-name">users-symbolic</property>
<property name="action-name">details.show-subpage</property>
<property name="action-target">'members'</property>
<property name="activatable">True</property>
<child type="suffix">
<object class="GtkLabel" id="members_count">
<property name="valign">center</property>
<property name="halign">center</property>
<property name="accessible-role">presentation</property>
</object>
</child>
<child type="suffix">
<object class="GtkImage">
<property name="valign">center</property>
<property name="halign">center</property>
<property name="icon-name">go-next-symbolic</property>
<property name="accessible-role">presentation</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">Media</property>
<property name="action-name">details.show-subpage</property>
<property name="action-target">'media-history'</property>
<property name="activatable">True</property>
<child type="suffix">
<object class="GtkImage">
<property name="valign">center</property>
<property name="halign">center</property>
<property name="icon-name">go-next-symbolic</property>
<property name="accessible-role">presentation</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">File</property>
<property name="action-name">details.show-subpage</property>
<property name="action-target">'file-history'</property>
<property name="activatable">True</property>
<child type="suffix">
<object class="GtkImage">
<property name="valign">center</property>
<property name="halign">center</property>
<property name="icon-name">go-next-symbolic</property>
<property name="accessible-role">presentation</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<!-- Translators: As in 'Audio file'. -->
<property name="title" translatable="yes">Audio</property>
<property name="action-name">details.show-subpage</property>
<property name="action-target">'audio-history'</property>
<property name="activatable">True</property>
<child type="suffix">
<object class="GtkImage">
<property name="valign">center</property>
<property name="halign">center</property>
<property name="icon-name">go-next-symbolic</property>
<property name="accessible-role">presentation</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<style>
<class name="room-details-group"/>
</style>
<child>
<object class="ComponentsEditableAvatar" id="avatar">
<binding name="data">
<lookup name="avatar-data">
<lookup name="room">ContentRoomDetailsGeneralPage</lookup>
</lookup>
</binding>
</object>
</child>
<child>
<object class="GtkBox">
<property name="spacing">6</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkEntry" id="room_name_entry">
<property name="sensitive">false</property>
<property name="activates-default">True</property>
<property name="xalign">0.5</property>
<property name="buffer">
<object class="GtkEntryBuffer" id="room_name_buffer">
<binding name="text">
<lookup name="display-name">
<lookup name="room">ContentRoomDetailsGeneralPage</lookup>
</lookup>
</binding>
</object>
</property>
<style>
<class name="room-details-name"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel" id="room_topic_label">
<property name="visible">false</property>
<property name="margin-top">12</property>
<property name="label" translatable="yes">Description</property>
<property name="halign">start</property>
<style>
<class name="dim-label"/>
<class name="caption-heading"/>
</style>
</object>
</child>
<child>
<object class="CustomEntry" id="room_topic_entry">
<property name="sensitive">false</property>
<property name="margin-bottom">18</property>
<child>
<object class="GtkTextView" id="room_topic_text_view">
<property name="justification">center</property>
<property name="wrap-mode">word-char</property>
<property name="accepts-tab">False</property>
<property name="top-margin">7</property>
<property name="bottom-margin">7</property>
<property name="buffer">
<object class="GtkTextBuffer" id="room_topic_buffer">
<binding name="text">
<closure type="gchararray" function="unwrap_string_or_empty">
<lookup name="topic">
<lookup name="room">ContentRoomDetailsGeneralPage</lookup>
</lookup>
</closure>
</binding>
</object>
</property>
</object>
</child>
<style>
<class name="room-details-topic"/>
</style>
</object>
</child>
<child>
<object class="GtkButton" id="edit_details_btn">
<property name="can-shrink">true</property>
<property name="halign">center</property>
<property name="label" translatable="yes">Edit Details</property>
<signal name="clicked" handler="edit_details_clicked" swapped="yes"/>
<style>
<class name="pill" />
</style>
</object>
</child>
<child>
<object class="SpinnerButton" id="save_details_btn">
<property name="visible" bind-source="ContentRoomDetailsGeneralPage" bind-property="edit-mode-enabled" bind-flags="sync-create"/>
<property name="halign">center</property>
<property name="label" translatable="yes">Save Details</property>
<signal name="clicked" handler="save_details_clicked" swapped="yes"/>
<style>
<class name="pill" />
</style>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">Members</property>
<property name="icon-name">users-symbolic</property>
<property name="action-name">details.show-subpage</property>
<property name="action-target">'members'</property>
<property name="activatable">True</property>
<child type="suffix">
<object class="GtkLabel" id="members_count">
<property name="valign">center</property>
<property name="halign">center</property>
<property name="accessible-role">presentation</property>
</object>
</child>
<child type="suffix">
<object class="GtkImage">
<property name="valign">center</property>
<property name="halign">center</property>
<property name="icon-name">go-next-symbolic</property>
<property name="accessible-role">presentation</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">Media</property>
<property name="action-name">details.show-subpage</property>
<property name="action-target">'media-history'</property>
<property name="activatable">True</property>
<child type="suffix">
<object class="GtkImage">
<property name="valign">center</property>
<property name="halign">center</property>
<property name="icon-name">go-next-symbolic</property>
<property name="accessible-role">presentation</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">File</property>
<property name="action-name">details.show-subpage</property>
<property name="action-target">'file-history'</property>
<property name="activatable">True</property>
<child type="suffix">
<object class="GtkImage">
<property name="valign">center</property>
<property name="halign">center</property>
<property name="icon-name">go-next-symbolic</property>
<property name="accessible-role">presentation</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<!-- Translators: As in 'Audio file'. -->
<property name="title" translatable="yes">Audio</property>
<property name="action-name">details.show-subpage</property>
<property name="action-target">'audio-history'</property>
<property name="activatable">True</property>
<child type="suffix">
<object class="GtkImage">
<property name="valign">center</property>
<property name="halign">center</property>
<property name="icon-name">go-next-symbolic</property>
<property name="accessible-role">presentation</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup" id="notifications">
<property name="title" translatable="yes">Notifications</property>
<property name="description" translatable="yes">Which messages trigger notifications in this room.</property>
<child>
<object class="AdwActionRow">
<property name="activatable-widget">notifications_global_radio</property>
<property name="title" translatable="yes">Use the global setting</property>
<child type="prefix">
<object class="LoadingBin" id="notifications_global_bin">
<property name="child">
<object class="GtkCheckButton" id="notifications_global_radio">
<property name="valign">center</property>
<property name="action-name">room.set-notifications-setting</property>
<property name="action-target">'global'</property>
</object>
</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="activatable-widget">notifications_all_radio</property>
<property name="title" translatable="yes">All messages</property>
<child type="prefix">
<object class="LoadingBin" id="notifications_all_bin">
<property name="child">
<object class="GtkCheckButton" id="notifications_all_radio">
<property name="valign">center</property>
<property name="action-name">room.set-notifications-setting</property>
<property name="action-target">'all'</property>
</object>
</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="activatable-widget">notifications_mentions_radio</property>
<property name="title" translatable="yes">Only mentions and keywords</property>
<child type="prefix">
<object class="LoadingBin" id="notifications_mentions_bin">
<property name="child">
<object class="GtkCheckButton" id="notifications_mentions_radio">
<property name="valign">center</property>
<property name="action-name">room.set-notifications-setting</property>
<property name="action-target">'mentions-only'</property>
</object>
</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="activatable-widget">notifications_mute_radio</property>
<property name="title" translatable="yes">Disable notifications</property>
<child type="prefix">
<object class="LoadingBin" id="notifications_mute_bin">
<property name="child">
<object class="GtkCheckButton" id="notifications_mute_radio">
<property name="valign">center</property>
<property name="action-name">room.set-notifications-setting</property>
<property name="action-target">'mute'</property>
</object>
</property>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>