From 3388795df5ee8bb3f78ebb7df51b9ab6ea79a704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 23 May 2025 10:54:47 +0200 Subject: [PATCH] session: Synchronize the media previews safety settings with the Matrix account data To share the setting between clients. --- po/POTFILES.in | 1 + src/components/avatar/mod.rs | 30 +- src/session/model/global_account_data.rs | 285 ++++++++++++++++++ src/session/model/mod.rs | 2 + src/session/model/notifications/mod.rs | 2 +- src/session/model/session.rs | 27 +- src/session/model/session_settings.rs | 184 ++++------- .../view/account_settings/safety_page/mod.rs | 225 +++++++++++--- .../view/account_settings/safety_page/mod.ui | 15 +- .../history_viewer/visual_media_item.rs | 5 +- .../room_history/message_row/visual_media.rs | 23 +- 11 files changed, 595 insertions(+), 204 deletions(-) create mode 100644 src/session/model/global_account_data.rs diff --git a/po/POTFILES.in b/po/POTFILES.in index b4cf331d..417b0276 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -101,6 +101,7 @@ src/session/view/account_settings/notifications_page.ui src/session/view/account_settings/safety_page/ignored_users_subpage/ignored_user_row.rs src/session/view/account_settings/safety_page/ignored_users_subpage/ignored_user_row.ui src/session/view/account_settings/safety_page/ignored_users_subpage/mod.ui +src/session/view/account_settings/safety_page/mod.rs src/session/view/account_settings/safety_page/mod.ui src/session/view/account_settings/user_session/user_session_list_subpage.ui src/session/view/account_settings/user_session/user_session_row.ui diff --git a/src/components/avatar/mod.rs b/src/components/avatar/mod.rs index 63547262..4b136008 100644 --- a/src/components/avatar/mod.rs +++ b/src/components/avatar/mod.rs @@ -80,7 +80,7 @@ mod imp { paintable_ref: RefCell>, paintable_animation_ref: RefCell>, watched_room_handler: RefCell>, - watched_session_settings_handler: RefCell>, + watched_global_account_data_handler: RefCell>, } #[glib::object_subclass] @@ -200,8 +200,8 @@ mod imp { )); self.watched_room_handler.replace(Some(room_handler)); - let session_settings_handler = session - .settings() + let global_account_data_handler = session + .global_account_data() .connect_media_previews_enabled_changed(clone!( #[weak(rename_to = imp)] self, @@ -209,8 +209,8 @@ mod imp { imp.update_paintable(); } )); - self.watched_session_settings_handler - .replace(Some(session_settings_handler)); + self.watched_global_account_data_handler + .replace(Some(global_account_data_handler)); } AvatarImageSafetySetting::InviteAvatars => { let room_handler = room.connect_is_invite_notify(clone!( @@ -222,8 +222,8 @@ mod imp { )); self.watched_room_handler.replace(Some(room_handler)); - let session_settings_handler = session - .settings() + let global_account_data_handler = session + .global_account_data() .connect_invite_avatars_enabled_notify(clone!( #[weak(rename_to = imp)] self, @@ -231,8 +231,8 @@ mod imp { imp.update_paintable(); } )); - self.watched_session_settings_handler - .replace(Some(session_settings_handler)); + self.watched_global_account_data_handler + .replace(Some(global_account_data_handler)); } } @@ -246,9 +246,9 @@ mod imp { room.disconnect(handler); } - if let Some(handler) = self.watched_session_settings_handler.take() { + if let Some(handler) = self.watched_global_account_data_handler.take() { room.session() - .inspect(|session| session.settings().disconnect(handler)); + .inspect(|session| session.global_account_data().disconnect(handler)); } } } @@ -271,11 +271,11 @@ mod imp { match watched_safety_setting { AvatarImageSafetySetting::None => unreachable!(), - AvatarImageSafetySetting::MediaPreviews => { - session.settings().should_room_show_media_previews(&room) - } + AvatarImageSafetySetting::MediaPreviews => session + .global_account_data() + .should_room_show_media_previews(&room), AvatarImageSafetySetting::InviteAvatars => { - !room.is_invite() || session.settings().invite_avatars_enabled() + !room.is_invite() || session.global_account_data().invite_avatars_enabled() } } } diff --git a/src/session/model/global_account_data.rs b/src/session/model/global_account_data.rs new file mode 100644 index 00000000..c908b824 --- /dev/null +++ b/src/session/model/global_account_data.rs @@ -0,0 +1,285 @@ +use futures_util::StreamExt; +use gtk::{ + glib, + glib::{clone, closure_local}, + prelude::*, + subclass::prelude::*, +}; +use ruma::events::media_preview_config::{ + InviteAvatars, MediaPreviewConfigEventContent, MediaPreviews, +}; +use tokio::task::AbortHandle; +use tracing::error; + +use super::{Room, Session}; +use crate::{spawn, spawn_tokio}; + +mod imp { + use std::{ + cell::{Cell, OnceCell, RefCell}, + sync::LazyLock, + }; + + use glib::subclass::Signal; + + use super::*; + + #[derive(Debug, Default, glib::Properties)] + #[properties(wrapper_type = super::GlobalAccountData)] + pub struct GlobalAccountData { + /// The session this account data belongs to. + #[property(get, construct_only)] + session: OnceCell, + /// Which rooms display media previews for this session. + pub(super) media_previews_enabled: RefCell, + /// Whether to display avatars in invites. + #[property(get, default = true)] + invite_avatars_enabled: Cell, + abort_handle: RefCell>, + } + + #[glib::object_subclass] + impl ObjectSubclass for GlobalAccountData { + const NAME: &'static str = "GlobalAccountData"; + type Type = super::GlobalAccountData; + } + + #[glib::derived_properties] + impl ObjectImpl for GlobalAccountData { + fn signals() -> &'static [Signal] { + static SIGNALS: LazyLock> = + LazyLock::new(|| vec![Signal::builder("media-previews-enabled-changed").build()]); + SIGNALS.as_ref() + } + + fn constructed(&self) { + self.parent_constructed(); + + spawn!(clone!( + #[weak(rename_to = imp)] + self, + async move { + imp.init_media_previews_settings().await; + imp.apply_migrations().await; + } + )); + } + + fn dispose(&self) { + if let Some(handle) = self.abort_handle.take() { + handle.abort(); + } + } + } + + impl GlobalAccountData { + /// The session these settings are for. + fn session(&self) -> &Session { + self.session.get().expect("session should be initialized") + } + + /// Initialize the media previews settings from the account data and + /// watch for changes. + pub(super) async fn init_media_previews_settings(&self) { + let client = self.session().client(); + let handle = + spawn_tokio!(async move { client.account().observe_media_preview_config().await }); + + let (account_data, stream) = match handle.await.expect("task was not aborted") { + Ok((account_data, stream)) => (account_data, stream), + Err(error) => { + error!("Could not initialize media preview settings: {error}"); + return; + } + }; + + self.update_media_previews_settings(account_data); + + let obj_weak = glib::SendWeakRef::from(self.obj().downgrade()); + let fut = stream.for_each(move |account_data| { + let obj_weak = obj_weak.clone(); + async move { + let ctx = glib::MainContext::default(); + ctx.spawn(async move { + spawn!(async move { + if let Some(obj) = obj_weak.upgrade() { + obj.imp().update_media_previews_settings(account_data); + } + }); + }); + } + }); + + let abort_handle = spawn_tokio!(fut).abort_handle(); + self.abort_handle.replace(Some(abort_handle)); + } + + /// Update the media previews settings with the given account data. + fn update_media_previews_settings(&self, account_data: MediaPreviewConfigEventContent) { + let media_previews_enabled_changed = + *self.media_previews_enabled.borrow() != account_data.media_previews; + if media_previews_enabled_changed { + *self.media_previews_enabled.borrow_mut() = account_data.media_previews; + self.obj() + .emit_by_name::<()>("media-previews-enabled-changed", &[]); + } + + let account_data_invite_avatars_enabled = + account_data.invite_avatars == InviteAvatars::On; + let invite_avatars_enabled_changed = + self.invite_avatars_enabled.get() != account_data_invite_avatars_enabled; + if invite_avatars_enabled_changed { + self.invite_avatars_enabled + .set(account_data_invite_avatars_enabled); + self.obj().notify_invite_avatars_enabled(); + } + } + + /// Apply any necessary migrations. + pub(super) async fn apply_migrations(&self) { + let session_settings = self.session().settings(); + let mut stored_settings = session_settings.stored_settings(); + + if stored_settings.version != 0 { + // No migration to apply. + return; + } + + // Align the account data with the stored settings. + let stored_media_previews_enabled = stored_settings + .media_previews_enabled + .take() + .map(|setting| setting.global) + .unwrap_or_default(); + let _ = self + .set_media_previews_enabled(stored_media_previews_enabled.into()) + .await; + + let stored_media_previews_enabled = stored_settings + .invite_avatars_enabled + .take() + .unwrap_or(true); + let _ = self + .set_invite_avatars_enabled(stored_media_previews_enabled) + .await; + + session_settings.apply_version_1_migration(); + } + + /// Set which rooms display media previews. + pub(super) async fn set_media_previews_enabled( + &self, + setting: MediaPreviews, + ) -> Result<(), ()> { + if *self.media_previews_enabled.borrow() == setting { + return Ok(()); + } + + let client = self.session().client(); + let setting_clone = setting.clone(); + let handle = spawn_tokio!(async move { + client + .account() + .set_media_previews_display_policy(setting_clone) + .await + }); + + if let Err(error) = handle.await.expect("task was not aborted") { + error!("Could not change media previews enabled setting: {error}"); + return Err(()); + } + + self.media_previews_enabled.replace(setting); + + self.obj() + .emit_by_name::<()>("media-previews-enabled-changed", &[]); + + Ok(()) + } + + /// Set whether to display avatars in invites. + pub(super) async fn set_invite_avatars_enabled(&self, enabled: bool) -> Result<(), ()> { + if self.invite_avatars_enabled.get() == enabled { + return Ok(()); + } + + let client = self.session().client(); + let setting = if enabled { + InviteAvatars::On + } else { + InviteAvatars::Off + }; + let handle = spawn_tokio!(async move { + client + .account() + .set_invite_avatars_display_policy(setting) + .await + }); + + if let Err(error) = handle.await.expect("task was not aborted") { + error!("Could not change invite avatars enabled setting: {error}"); + return Err(()); + } + + self.invite_avatars_enabled.set(enabled); + self.obj().notify_invite_avatars_enabled(); + + Ok(()) + } + } +} + +glib::wrapper! { + /// The settings in the global account data of a [`Session`]. + pub struct GlobalAccountData(ObjectSubclass); +} + +impl GlobalAccountData { + /// Create a new `GlobalAccountData` for the given session. + pub(crate) fn new(session: &Session) -> Self { + glib::Object::builder::() + .property("session", session) + .build() + } + + /// Which rooms display media previews. + pub(crate) fn media_previews_enabled(&self) -> MediaPreviews { + self.imp().media_previews_enabled.borrow().clone() + } + + /// Whether the given room should display media previews. + pub(crate) fn should_room_show_media_previews(&self, room: &Room) -> bool { + match &*self.imp().media_previews_enabled.borrow() { + MediaPreviews::Off => false, + MediaPreviews::Private => !room.join_rule().anyone_can_join(), + _ => true, + } + } + + /// Set which rooms display media previews. + pub(crate) async fn set_media_previews_enabled( + &self, + setting: MediaPreviews, + ) -> Result<(), ()> { + self.imp().set_media_previews_enabled(setting).await + } + + /// Set whether to display avatars in invites. + pub(crate) async fn set_invite_avatars_enabled(&self, enabled: bool) -> Result<(), ()> { + self.imp().set_invite_avatars_enabled(enabled).await + } + + /// Connect to the signal emitted when the media previews setting changed. + pub fn connect_media_previews_enabled_changed( + &self, + f: F, + ) -> glib::SignalHandlerId { + self.connect_closure( + "media-previews-enabled-changed", + true, + closure_local!(move |obj: Self| { + f(&obj); + }), + ) + } +} diff --git a/src/session/model/mod.rs b/src/session/model/mod.rs index 6627b082..14078752 100644 --- a/src/session/model/mod.rs +++ b/src/session/model/mod.rs @@ -1,3 +1,4 @@ +mod global_account_data; mod ignored_users; mod notifications; mod remote; @@ -12,6 +13,7 @@ mod user_sessions_list; mod verification; pub(crate) use self::{ + global_account_data::*, ignored_users::IgnoredUsers, notifications::{ Notifications, NotificationsGlobalSetting, NotificationsRoomSetting, NotificationsSettings, diff --git a/src/session/model/notifications/mod.rs b/src/session/model/notifications/mod.rs index eb1e60e0..73daf6eb 100644 --- a/src/session/model/notifications/mod.rs +++ b/src/session/model/notifications/mod.rs @@ -279,7 +279,7 @@ impl Notifications { format!("{session_id}//{matrix_uri}//{random_id}") }; - let inhibit_image = is_invite && !session.settings().invite_avatars_enabled(); + let inhibit_image = is_invite && !session.global_account_data().invite_avatars_enabled(); let icon = room.avatar_data().as_notification_icon(inhibit_image).await; Self::send_notification( diff --git a/src/session/model/session.rs b/src/session/model/session.rs index ad8a32d6..667303d1 100644 --- a/src/session/model/session.rs +++ b/src/session/model/session.rs @@ -18,8 +18,8 @@ use tokio_stream::wrappers::BroadcastStream; use tracing::{debug, error, info}; use super::{ - IgnoredUsers, Notifications, RemoteCache, RoomList, SessionSecurity, SessionSettings, - SidebarItemList, SidebarListModel, User, UserSessionsList, VerificationList, + GlobalAccountData, IgnoredUsers, Notifications, RemoteCache, RoomList, SessionSecurity, + SessionSettings, SidebarItemList, SidebarListModel, User, UserSessionsList, VerificationList, }; use crate::{ components::AvatarData, @@ -91,6 +91,9 @@ mod imp { /// The current settings for this session. #[property(get, construct_only)] settings: OnceCell, + /// The settings in the global account data for this session. + #[property(get = Self::global_account_data_owned)] + global_account_data: OnceCell, /// The notifications API for this session. #[property(get)] notifications: Notifications, @@ -335,8 +338,20 @@ mod imp { self.obj().notify_is_offline(); } + /// The settings stored in the global account data for this session. + fn global_account_data(&self) -> &GlobalAccountData { + self.global_account_data + .get_or_init(|| GlobalAccountData::new(&self.obj())) + } + + /// The owned settings stored in the global account data for this + /// session. + fn global_account_data_owned(&self) -> GlobalAccountData { + self.global_account_data().clone() + } + /// The cache for remote data. - pub(crate) fn remote_cache(&self) -> &RemoteCache { + pub(super) fn remote_cache(&self) -> &RemoteCache { self.remote_cache .get_or_init(|| RemoteCache::new(self.obj().clone())) } @@ -356,6 +371,8 @@ mod imp { } ) ); + + self.global_account_data(); self.watch_session_changes(); self.update_homeserver_reachable().await; @@ -540,7 +557,7 @@ mod imp { Err(error) => { error!( session = self.obj().session_id(), - "Failed to deserialize session profile: {error}" + "Could not deserialize session profile: {error}" ); return; } @@ -611,7 +628,7 @@ mod imp { Err(error) => { error!( session = self.obj().session_id(), - "Failed to serialize session profile: {error}" + "Could not serialize session profile: {error}" ); return; } diff --git a/src/session/model/session_settings.rs b/src/session/model/session_settings.rs index d6e6e678..bdffe7c5 100644 --- a/src/session/model/session_settings.rs +++ b/src/session/model/session_settings.rs @@ -1,16 +1,24 @@ use std::collections::BTreeSet; -use gtk::{glib, glib::closure_local, prelude::*, subclass::prelude::*}; +use gtk::{glib, prelude::*, subclass::prelude::*}; use indexmap::IndexSet; -use ruma::{serde::SerializeAsRefStr, OwnedServerName}; +use ruma::{events::media_preview_config::MediaPreviews, OwnedServerName}; use serde::{Deserialize, Serialize}; +use tracing::info; -use super::{Room, SidebarSectionName}; +use super::SidebarSectionName; use crate::{session_list::SessionListSettings, Application}; +/// The current version of the stored session settings. +const CURRENT_VERSION: u8 = 1; + #[derive(Debug, Clone, Serialize, Deserialize)] #[allow(clippy::struct_excessive_bools)] pub(crate) struct StoredSessionSettings { + /// The version of the stored settings. + #[serde(default)] + pub(super) version: u8, + /// Custom servers to explore. #[serde(default, skip_serializing_if = "IndexSet::is_empty")] explore_custom_servers: IndexSet, @@ -41,27 +49,29 @@ pub(crate) struct StoredSessionSettings { sections_expanded: SectionsExpanded, /// Which rooms display media previews for this session. - #[serde(default, skip_serializing_if = "ruma::serde::is_default")] - media_previews_enabled: MediaPreviewsSetting, + /// + /// Legacy setting from version 0 of the stored settings. + #[serde(skip_serializing)] + pub(super) media_previews_enabled: Option, /// Whether to display avatars in invites. - #[serde( - default = "ruma::serde::default_true", - skip_serializing_if = "ruma::serde::is_true" - )] - invite_avatars_enabled: bool, + /// + /// Legacy setting from version 0 of the stored settings. + #[serde(skip_serializing)] + pub(super) invite_avatars_enabled: Option, } impl Default for StoredSessionSettings { fn default() -> Self { Self { + version: CURRENT_VERSION, explore_custom_servers: Default::default(), notifications_enabled: true, public_read_receipts_enabled: true, typing_enabled: true, sections_expanded: Default::default(), media_previews_enabled: Default::default(), - invite_avatars_enabled: true, + invite_avatars_enabled: Default::default(), } } } @@ -70,11 +80,8 @@ mod imp { use std::{ cell::{OnceCell, RefCell}, marker::PhantomData, - sync::LazyLock, }; - use glib::subclass::Signal; - use super::*; #[derive(Debug, Default, glib::Properties)] @@ -94,9 +101,6 @@ mod imp { /// Whether typing notifications are enabled for this session. #[property(get = Self::typing_enabled, set = Self::set_typing_enabled, explicit_notify, default = true)] typing_enabled: PhantomData, - /// Whether to display avatars in invites. - #[property(get = Self::invite_avatars_enabled, set = Self::set_invite_avatars_enabled, explicit_notify, default = true)] - invite_avatars_enabled: PhantomData, } #[glib::object_subclass] @@ -106,13 +110,7 @@ mod imp { } #[glib::derived_properties] - impl ObjectImpl for SessionSettings { - fn signals() -> &'static [Signal] { - static SIGNALS: LazyLock> = - LazyLock::new(|| vec![Signal::builder("media-previews-enabled-changed").build()]); - SIGNALS.as_ref() - } - } + impl ObjectImpl for SessionSettings {} impl SessionSettings { /// Whether notifications are enabled for this session. @@ -165,26 +163,33 @@ mod imp { self.obj().notify_typing_enabled(); } - /// Whether to display avatars in invites. - fn invite_avatars_enabled(&self) -> bool { - self.stored_settings.borrow().invite_avatars_enabled - } + /// Apply the migration of the stored settings from version 0 to version + /// 1. + pub(crate) fn apply_version_1_migration(&self) { + { + let mut stored_settings = self.stored_settings.borrow_mut(); - /// Set whether to display avatars in invites. - fn set_invite_avatars_enabled(&self, enabled: bool) { - if self.invite_avatars_enabled() == enabled { - return; + if stored_settings.version > 0 { + return; + } + + info!( + session = self.obj().session_id(), + "Migrating store session to version 1" + ); + + stored_settings.media_previews_enabled.take(); + stored_settings.invite_avatars_enabled.take(); + stored_settings.version = 1; } - self.stored_settings.borrow_mut().invite_avatars_enabled = enabled; session_list_settings().save(); - self.obj().notify_invite_avatars_enabled(); } } } glib::wrapper! { - /// The settings of a `Session`. + /// The settings of a [`Session`](super::Session). pub struct SessionSettings(ObjectSubclass); } @@ -199,9 +204,7 @@ impl SessionSettings { /// Restore existing `SessionSettings` with the given session ID and stored /// settings. pub(crate) fn restore(session_id: &str, stored_settings: StoredSessionSettings) -> Self { - let obj = glib::Object::builder::() - .property("session-id", session_id) - .build(); + let obj = Self::new(session_id); *obj.imp().stored_settings.borrow_mut() = stored_settings; obj } @@ -211,6 +214,11 @@ impl SessionSettings { self.imp().stored_settings.borrow().clone() } + /// Apply the migration of the stored settings from version 0 to version 1. + pub(crate) fn apply_version_1_migration(&self) { + self.imp().apply_version_1_migration(); + } + /// Delete the settings from the application settings. pub(crate) fn delete(&self) { session_list_settings().remove(&self.session_id()); @@ -256,49 +264,6 @@ impl SessionSettings { .set_section_expanded(section_name, expanded); session_list_settings().save(); } - - /// Whether the given room should display media previews. - pub(crate) fn should_room_show_media_previews(&self, room: &Room) -> bool { - self.imp() - .stored_settings - .borrow() - .media_previews_enabled - .should_room_show_media_previews(room) - } - - /// Which rooms display media previews. - pub(crate) fn media_previews_global_enabled(&self) -> MediaPreviewsGlobalSetting { - self.imp() - .stored_settings - .borrow() - .media_previews_enabled - .global - } - - /// Set which rooms display media previews. - pub(crate) fn set_media_previews_global_enabled(&self, setting: MediaPreviewsGlobalSetting) { - self.imp() - .stored_settings - .borrow_mut() - .media_previews_enabled - .global = setting; - session_list_settings().save(); - self.emit_by_name::<()>("media-previews-enabled-changed", &[]); - } - - /// Connect to the signal emitted when the media previews setting changed. - pub fn connect_media_previews_enabled_changed( - &self, - f: F, - ) -> glib::SignalHandlerId { - self.connect_closure( - "media-previews-enabled-changed", - true, - closure_local!(move |obj: Self| { - f(&obj); - }), - ) - } } /// The sections that are expanded. @@ -338,35 +303,22 @@ impl Default for SectionsExpanded { } /// Setting about which rooms display media previews. -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub(crate) struct MediaPreviewsSetting { +/// +/// Legacy setting from version 0 of the stored settings. +#[derive(Debug, Clone, Default, Deserialize)] +pub(super) struct MediaPreviewsSetting { /// The default setting for all rooms. - #[serde(default, skip_serializing_if = "ruma::serde::is_default")] - global: MediaPreviewsGlobalSetting, -} - -impl MediaPreviewsSetting { - // Whether the given room should show room previews according to this setting. - pub(crate) fn should_room_show_media_previews(&self, room: &Room) -> bool { - self.global.should_room_show_media_previews(room) - } + #[serde(default)] + pub(super) global: MediaPreviewsGlobalSetting, } /// Possible values of the global setting about which rooms display media /// previews. -#[derive( - Debug, - Clone, - Copy, - Default, - PartialEq, - Eq, - strum::AsRefStr, - strum::EnumString, - SerializeAsRefStr, -)] -#[strum(serialize_all = "kebab-case")] -pub(crate) enum MediaPreviewsGlobalSetting { +/// +/// Legacy setting from version 0 of the stored settings. +#[derive(Debug, Clone, Default, strum::EnumString)] +#[strum(serialize_all = "lowercase")] +pub(super) enum MediaPreviewsGlobalSetting { /// All rooms show media previews. All, /// Only private rooms show media previews. @@ -376,18 +328,6 @@ pub(crate) enum MediaPreviewsGlobalSetting { None, } -impl MediaPreviewsGlobalSetting { - /// Whether the given room should show room previews according to this - /// setting. - pub(crate) fn should_room_show_media_previews(self, room: &Room) -> bool { - match self { - Self::All => true, - Self::Private => !room.join_rule().anyone_can_join(), - Self::None => false, - } - } -} - impl<'de> Deserialize<'de> for MediaPreviewsGlobalSetting { fn deserialize(deserializer: D) -> Result where @@ -398,6 +338,16 @@ impl<'de> Deserialize<'de> for MediaPreviewsGlobalSetting { } } +impl From for MediaPreviews { + fn from(value: MediaPreviewsGlobalSetting) -> Self { + match value { + MediaPreviewsGlobalSetting::All => Self::On, + MediaPreviewsGlobalSetting::Private => Self::Private, + MediaPreviewsGlobalSetting::None => Self::Off, + } + } +} + /// The session list settings of the application. fn session_list_settings() -> SessionListSettings { Application::default().session_list().settings() diff --git a/src/session/view/account_settings/safety_page/mod.rs b/src/session/view/account_settings/safety_page/mod.rs index bd461199..09969760 100644 --- a/src/session/view/account_settings/safety_page/mod.rs +++ b/src/session/view/account_settings/safety_page/mod.rs @@ -1,17 +1,23 @@ use adw::{prelude::*, subclass::prelude::*}; +use gettextrs::gettext; use gtk::{glib, glib::clone, CompositeTemplate}; +use ruma::events::media_preview_config::MediaPreviews; use tracing::error; mod ignored_users_subpage; pub(super) use self::ignored_users_subpage::IgnoredUsersSubpage; use crate::{ - components::ButtonCountRow, - session::model::{MediaPreviewsGlobalSetting, Session}, + components::{ButtonCountRow, CheckLoadingRow, SwitchLoadingRow}, + session::model::Session, + spawn, toast, }; mod imp { - use std::{cell::RefCell, marker::PhantomData}; + use std::{ + cell::{Cell, RefCell}, + marker::PhantomData, + }; use glib::subclass::InitializingObject; @@ -28,15 +34,29 @@ mod imp { #[template_child] ignored_users_row: TemplateChild, #[template_child] - invite_avatars_row: TemplateChild, + media_previews: TemplateChild, + #[template_child] + media_previews_on_row: TemplateChild, + #[template_child] + media_previews_private_row: TemplateChild, + #[template_child] + media_previews_off_row: TemplateChild, + #[template_child] + invite_avatars_row: TemplateChild, /// The current session. #[property(get, set = Self::set_session, nullable)] session: glib::WeakRef, /// The media previews setting, as a string. #[property(get = Self::media_previews_enabled, set = Self::set_media_previews_enabled)] media_previews_enabled: PhantomData, + /// Whether the media previews section is busy. + #[property(get)] + media_previews_loading: Cell, + /// Whether the invite avatars row is busy. + #[property(get)] + invite_avatars_loading: Cell, ignored_users_count_handler: RefCell>, - session_settings_handler: RefCell>, + global_account_data_handlers: RefCell>, bindings: RefCell>, } @@ -48,6 +68,7 @@ mod imp { fn class_init(klass: &mut Self::Class) { Self::bind_template(klass); + Self::bind_template_callbacks(klass); klass.install_property_action( "safety.set-media-previews-enabled", @@ -63,13 +84,14 @@ mod imp { #[glib::derived_properties] impl ObjectImpl for SafetyPage { fn dispose(&self) { - self.clear(); + self.disconnect_signals(); } } impl WidgetImpl for SafetyPage {} impl PreferencesPageImpl for SafetyPage {} + #[gtk::template_callbacks] impl SafetyPage { /// Set the current session. fn set_session(&self, session: Option<&Session>) { @@ -77,8 +99,7 @@ mod imp { return; } - self.clear(); - let obj = self.obj(); + self.disconnect_signals(); if let Some(session) = session { let ignored_users = session.ignored_users(); @@ -96,19 +117,28 @@ mod imp { self.ignored_users_count_handler .replace(Some(ignored_users_count_handler)); - let session_settings = session.settings(); + let global_account_data = session.global_account_data(); - let media_previews_handler = session_settings + let media_previews_handler = global_account_data .connect_media_previews_enabled_changed(clone!( - #[weak] - obj, + #[weak(rename_to = imp)] + self, + move |_| { + imp.update_media_previews(); + } + )); + let invite_avatars_handler = global_account_data + .connect_invite_avatars_enabled_notify(clone!( + #[weak(rename_to = imp)] + self, move |_| { - // Update the active media previews radio button. - obj.notify_media_previews_enabled(); + imp.update_invite_avatars(); } )); - self.session_settings_handler - .replace(Some(media_previews_handler)); + self.global_account_data_handlers + .replace(vec![media_previews_handler, invite_avatars_handler]); + + let session_settings = session.settings(); let public_read_receipts_binding = session_settings .bind_property( @@ -124,28 +154,16 @@ mod imp { .bidirectional() .sync_create() .build(); - let invite_avatars_binding = session_settings - .bind_property( - "invite-avatars-enabled", - &*self.invite_avatars_row, - "active", - ) - .bidirectional() - .sync_create() - .build(); - self.bindings.replace(vec![ - public_read_receipts_binding, - typing_binding, - invite_avatars_binding, - ]); + self.bindings + .replace(vec![public_read_receipts_binding, typing_binding]); } self.session.set(session); - // Update the active media previews radio button. - obj.notify_media_previews_enabled(); - obj.notify_session(); + self.update_media_previews(); + self.update_invite_avatars(); + self.obj().notify_session(); } /// The media previews setting, as a string. @@ -154,38 +172,147 @@ mod imp { return String::new(); }; - session - .settings() - .media_previews_global_enabled() - .as_ref() - .to_owned() + match session.global_account_data().media_previews_enabled() { + MediaPreviews::Off => "off", + MediaPreviews::Private => "private", + _ => "on", + } + .to_owned() + } + + /// Update the media previews section. + fn update_media_previews(&self) { + // Updates the active radio button. + self.obj().notify_media_previews_enabled(); + + self.media_previews + .set_sensitive(!self.media_previews_loading.get()); } /// Set the media previews setting, as a string. fn set_media_previews_enabled(&self, setting: &str) { + if setting.is_empty() { + error!("Invalid empty value to set media previews setting"); + return; + } + + let setting = setting.into(); + + spawn!(clone!( + #[weak(rename_to = imp)] + self, + async move { + imp.set_media_previews_enabled_inner(setting).await; + } + )); + } + + /// Propagate the media previews setting. + async fn set_media_previews_enabled_inner(&self, setting: MediaPreviews) { let Some(session) = self.session.upgrade() else { return; }; + let global_account_data = session.global_account_data(); - let Ok(setting) = setting.parse::() else { - error!("Invalid value to set global media previews setting: {setting}"); + if setting == global_account_data.media_previews_enabled() { + // Nothing to do. + return; + } + + self.media_previews.set_sensitive(false); + self.set_media_previews_loading(true, &setting); + + if global_account_data + .set_media_previews_enabled(setting.clone()) + .await + .is_err() + { + toast!( + self.obj(), + gettext("Could not change media previews setting"), + ); + } + + self.set_media_previews_loading(false, &setting); + self.update_media_previews(); + } + + /// Set the loading state of the media previews section. + fn set_media_previews_loading(&self, loading: bool, setting: &MediaPreviews) { + // Only show the spinner on the selected one. + self.media_previews_on_row + .set_is_loading(loading && *setting == MediaPreviews::On); + self.media_previews_private_row + .set_is_loading(loading && *setting == MediaPreviews::Private); + self.media_previews_off_row + .set_is_loading(loading && *setting == MediaPreviews::Off); + + self.media_previews_loading.set(loading); + self.obj().notify_media_previews_loading(); + } + + /// Update the invite avatars section. + fn update_invite_avatars(&self) { + let Some(session) = self.session.upgrade() else { return; }; - session - .settings() - .set_media_previews_global_enabled(setting); + self.invite_avatars_row + .set_is_active(session.global_account_data().invite_avatars_enabled()); + self.invite_avatars_row + .set_sensitive(!self.invite_avatars_loading.get()); } - /// Reset the signal handlers and bindings. - fn clear(&self) { + /// Set the invite avatars setting. + #[template_callback] + async fn set_invite_avatars_enabled(&self) { + let Some(session) = self.session.upgrade() else { + return; + }; + let global_account_data = session.global_account_data(); + + let enabled = self.invite_avatars_row.is_active(); + if enabled == global_account_data.invite_avatars_enabled() { + // Nothing to do. + return; + } + + self.invite_avatars_row.set_sensitive(false); + self.set_invite_avatars_loading(true); + + if global_account_data + .set_invite_avatars_enabled(enabled) + .await + .is_err() + { + let msg = if enabled { + gettext("Could not enable avatars for invites") + } else { + gettext("Could not disable avatars for invites") + }; + toast!(self.obj(), msg); + } + + self.set_invite_avatars_loading(false); + self.update_invite_avatars(); + } + + /// Set the loading state of the invite avatars section. + fn set_invite_avatars_loading(&self, loading: bool) { + self.invite_avatars_loading.set(loading); + self.obj().notify_invite_avatars_loading(); + } + + /// Disconnect the signal handlers and bindings. + fn disconnect_signals(&self) { if let Some(session) = self.session.upgrade() { - if let Some(handler) = self.ignored_users_count_handler.take() { - session.ignored_users().disconnect(handler); + let global_account_data = session.global_account_data(); + for handler in self.global_account_data_handlers.take() { + global_account_data.disconnect(handler); } - if let Some(handler) = self.session_settings_handler.take() { - session.settings().disconnect(handler); + if let Some(handler) = self.ignored_users_count_handler.take() { + session.ignored_users().disconnect(handler); } } diff --git a/src/session/view/account_settings/safety_page/mod.ui b/src/session/view/account_settings/safety_page/mod.ui index 0d8907e4..cd66ac1e 100644 --- a/src/session/view/account_settings/safety_page/mod.ui +++ b/src/session/view/account_settings/safety_page/mod.ui @@ -36,14 +36,14 @@ - + Media Previews Which rooms automatically show previews for images and videos. Hidden previews can always be shown by clicking on the media. - + Show in all rooms safety.set-media-previews-enabled - 'all' + 'on' @@ -54,10 +54,10 @@ - + Hide in all rooms safety.set-media-previews-enabled - 'none' + 'off' @@ -65,10 +65,11 @@ - - False + Show Avatars for Invites Display the avatars of the room and the inviter + + diff --git a/src/session/view/content/room_details/history_viewer/visual_media_item.rs b/src/session/view/content/room_details/history_viewer/visual_media_item.rs index 42981820..73fd20f5 100644 --- a/src/session/view/content/room_details/history_viewer/visual_media_item.rs +++ b/src/session/view/content/room_details/history_viewer/visual_media_item.rs @@ -160,7 +160,10 @@ mod imp { return; }; - if session.settings().should_room_show_media_previews(&room) { + if session + .global_account_data() + .should_room_show_media_previews(&room) + { spawn!( glib::Priority::LOW, clone!( diff --git a/src/session/view/content/room_history/message_row/visual_media.rs b/src/session/view/content/room_history/message_row/visual_media.rs index 57a1357b..5369bf49 100644 --- a/src/session/view/content/room_history/message_row/visual_media.rs +++ b/src/session/view/content/room_history/message_row/visual_media.rs @@ -71,7 +71,7 @@ mod imp { /// The room where the message was sent. room: glib::WeakRef, join_rule_handler: RefCell>, - session_settings_handler: RefCell>, + global_account_data_handler: RefCell>, /// The visual media message to display. media_message: RefCell>, /// The cache key for the current media message. @@ -298,7 +298,9 @@ mod imp { self.spinner.set_visible(state == LoadingState::Loading); self.hide_preview_button.set_visible( state == LoadingState::Ready - && !session.settings().should_room_show_media_previews(&room), + && !session + .global_account_data() + .should_room_show_media_previews(&room), ); self.error.set_visible(state == LoadingState::Error); @@ -482,8 +484,8 @@ mod imp { )); self.join_rule_handler.replace(Some(join_rule_handler)); - let session_settings_handler = session - .settings() + let global_account_data_handler = session + .global_account_data() .connect_media_previews_enabled_changed(clone!( #[weak(rename_to = imp)] self, @@ -491,8 +493,8 @@ mod imp { imp.update_media(); } )); - self.session_settings_handler - .replace(Some(session_settings_handler)); + self.global_account_data_handler + .replace(Some(global_account_data_handler)); self.room.set(Some(room)); @@ -595,7 +597,10 @@ mod imp { return; }; - if session.settings().should_room_show_media_previews(&room) { + if session + .global_account_data() + .should_room_show_media_previews(&room) + { // Only load the media if it was not loaded before. if self.state.get() == LoadingState::Initial { self.show_media(); @@ -796,9 +801,9 @@ mod imp { room.join_rule().disconnect(handler); } - if let Some(handler) = self.session_settings_handler.take() { + if let Some(handler) = self.global_account_data_handler.take() { if let Some(session) = room.session() { - session.settings().disconnect(handler); + session.global_account_data().disconnect(handler); } } }