Skip to main content

fractal/session/
global_account_data.rs

1use futures_util::StreamExt;
2use gtk::{
3    glib,
4    glib::{clone, closure_local},
5    prelude::*,
6    subclass::prelude::*,
7};
8use ruma::events::media_preview_config::{
9    InviteAvatars, MediaPreviewConfigEventContent, MediaPreviews,
10};
11use tokio::task::AbortHandle;
12use tracing::error;
13
14use super::{Room, Session};
15use crate::{session::JoinRuleValue, spawn, spawn_tokio};
16
17/// We default the media previews setting to private.
18const DEFAULT_MEDIA_PREVIEWS: MediaPreviews = MediaPreviews::Private;
19/// We enable the invite avatars by default.
20const DEFAULT_INVITE_AVATARS_ENABLED: bool = true;
21
22mod imp {
23    use std::{
24        cell::{Cell, OnceCell, RefCell},
25        sync::LazyLock,
26    };
27
28    use glib::subclass::Signal;
29
30    use super::*;
31
32    #[derive(Debug, glib::Properties)]
33    #[properties(wrapper_type = super::GlobalAccountData)]
34    pub struct GlobalAccountData {
35        /// The session this account data belongs to.
36        #[property(get, construct_only)]
37        session: OnceCell<Session>,
38        /// Which rooms display media previews for this session.
39        pub(super) media_previews_enabled: RefCell<MediaPreviews>,
40        /// Whether to display avatars in invites.
41        #[property(get, default = DEFAULT_INVITE_AVATARS_ENABLED)]
42        invite_avatars_enabled: Cell<bool>,
43        abort_handle: RefCell<Option<AbortHandle>>,
44    }
45
46    impl Default for GlobalAccountData {
47        fn default() -> Self {
48            Self {
49                session: Default::default(),
50                media_previews_enabled: RefCell::new(DEFAULT_MEDIA_PREVIEWS),
51                invite_avatars_enabled: Cell::new(DEFAULT_INVITE_AVATARS_ENABLED),
52                abort_handle: Default::default(),
53            }
54        }
55    }
56
57    #[glib::object_subclass]
58    impl ObjectSubclass for GlobalAccountData {
59        const NAME: &'static str = "GlobalAccountData";
60        type Type = super::GlobalAccountData;
61    }
62
63    #[glib::derived_properties]
64    impl ObjectImpl for GlobalAccountData {
65        fn signals() -> &'static [Signal] {
66            static SIGNALS: LazyLock<Vec<Signal>> =
67                LazyLock::new(|| vec![Signal::builder("media-previews-enabled-changed").build()]);
68            SIGNALS.as_ref()
69        }
70
71        fn constructed(&self) {
72            self.parent_constructed();
73
74            spawn!(clone!(
75                #[weak(rename_to = imp)]
76                self,
77                async move {
78                    imp.init_media_previews_settings().await;
79                    imp.apply_migrations().await;
80                }
81            ));
82        }
83
84        fn dispose(&self) {
85            if let Some(handle) = self.abort_handle.take() {
86                handle.abort();
87            }
88        }
89    }
90
91    impl GlobalAccountData {
92        /// The session these settings are for.
93        fn session(&self) -> &Session {
94            self.session.get().expect("session should be initialized")
95        }
96
97        /// Initialize the media previews settings from the account data and
98        /// watch for changes.
99        pub(super) async fn init_media_previews_settings(&self) {
100            let client = self.session().client();
101            let handle =
102                spawn_tokio!(async move { client.account().observe_media_preview_config().await });
103
104            let (account_data, stream) = match handle.await.expect("task was not aborted") {
105                Ok((account_data, stream)) => (account_data, stream),
106                Err(error) => {
107                    error!("Could not initialize media preview settings: {error}");
108                    return;
109                }
110            };
111
112            self.update_media_previews_settings(account_data.unwrap_or_default());
113
114            let obj_weak = glib::SendWeakRef::from(self.obj().downgrade());
115            let fut = stream.for_each(move |account_data| {
116                let obj_weak = obj_weak.clone();
117                async move {
118                    let ctx = glib::MainContext::default();
119                    ctx.spawn(async move {
120                        spawn!(async move {
121                            if let Some(obj) = obj_weak.upgrade() {
122                                obj.imp().update_media_previews_settings(account_data);
123                            }
124                        });
125                    });
126                }
127            });
128
129            let abort_handle = spawn_tokio!(fut).abort_handle();
130            self.abort_handle.replace(Some(abort_handle));
131        }
132
133        /// Update the media previews settings with the given account data.
134        fn update_media_previews_settings(&self, account_data: MediaPreviewConfigEventContent) {
135            let media_previews = account_data
136                .media_previews
137                .unwrap_or(DEFAULT_MEDIA_PREVIEWS);
138            let media_previews_enabled_changed =
139                *self.media_previews_enabled.borrow() != media_previews;
140            if media_previews_enabled_changed {
141                *self.media_previews_enabled.borrow_mut() = media_previews;
142                self.obj()
143                    .emit_by_name::<()>("media-previews-enabled-changed", &[]);
144            }
145
146            let invite_avatars_enabled = account_data
147                .invite_avatars
148                .map_or(DEFAULT_INVITE_AVATARS_ENABLED, |invite_avatars| {
149                    invite_avatars == InviteAvatars::On
150                });
151            let invite_avatars_enabled_changed =
152                self.invite_avatars_enabled.get() != invite_avatars_enabled;
153            if invite_avatars_enabled_changed {
154                self.invite_avatars_enabled.set(invite_avatars_enabled);
155                self.obj().notify_invite_avatars_enabled();
156            }
157        }
158
159        /// Apply any necessary migrations.
160        pub(super) async fn apply_migrations(&self) {
161            let session_settings = self.session().settings();
162            let mut stored_settings = session_settings.stored_settings();
163
164            if stored_settings.version != 0 {
165                // No migration to apply.
166                return;
167            }
168
169            // Align the account data with the stored settings.
170            let stored_media_previews_enabled = stored_settings
171                .media_previews_enabled
172                .take()
173                .map_or(DEFAULT_MEDIA_PREVIEWS, |setting| setting.global.into());
174            let _ = self
175                .set_media_previews_enabled(stored_media_previews_enabled)
176                .await;
177
178            let stored_invite_avatars_enabled = stored_settings
179                .invite_avatars_enabled
180                .take()
181                .unwrap_or(DEFAULT_INVITE_AVATARS_ENABLED);
182            let _ = self
183                .set_invite_avatars_enabled(stored_invite_avatars_enabled)
184                .await;
185
186            session_settings.apply_version_1_migration();
187        }
188
189        /// Set which rooms display media previews.
190        pub(super) async fn set_media_previews_enabled(
191            &self,
192            setting: MediaPreviews,
193        ) -> Result<(), ()> {
194            if *self.media_previews_enabled.borrow() == setting {
195                return Ok(());
196            }
197
198            let client = self.session().client();
199            let setting_clone = setting.clone();
200            let handle = spawn_tokio!(async move {
201                client
202                    .account()
203                    .set_media_previews_display_policy(setting_clone)
204                    .await
205            });
206
207            if let Err(error) = handle.await.expect("task was not aborted") {
208                error!("Could not change media previews enabled setting: {error}");
209                return Err(());
210            }
211
212            self.media_previews_enabled.replace(setting);
213
214            self.obj()
215                .emit_by_name::<()>("media-previews-enabled-changed", &[]);
216
217            Ok(())
218        }
219
220        /// Set whether to display avatars in invites.
221        pub(super) async fn set_invite_avatars_enabled(&self, enabled: bool) -> Result<(), ()> {
222            if self.invite_avatars_enabled.get() == enabled {
223                return Ok(());
224            }
225
226            let client = self.session().client();
227            let setting = if enabled {
228                InviteAvatars::On
229            } else {
230                InviteAvatars::Off
231            };
232            let handle = spawn_tokio!(async move {
233                client
234                    .account()
235                    .set_invite_avatars_display_policy(setting)
236                    .await
237            });
238
239            if let Err(error) = handle.await.expect("task was not aborted") {
240                error!("Could not change invite avatars enabled setting: {error}");
241                return Err(());
242            }
243
244            self.invite_avatars_enabled.set(enabled);
245            self.obj().notify_invite_avatars_enabled();
246
247            Ok(())
248        }
249    }
250}
251
252glib::wrapper! {
253    /// The settings in the global account data of a [`Session`].
254    pub struct GlobalAccountData(ObjectSubclass<imp::GlobalAccountData>);
255}
256
257impl GlobalAccountData {
258    /// Create a new `GlobalAccountData` for the given session.
259    pub(crate) fn new(session: &Session) -> Self {
260        glib::Object::builder::<Self>()
261            .property("session", session)
262            .build()
263    }
264
265    /// Which rooms display media previews.
266    pub(crate) fn media_previews_enabled(&self) -> MediaPreviews {
267        self.imp().media_previews_enabled.borrow().clone()
268    }
269
270    /// Whether the given room should display media previews.
271    pub(crate) fn should_room_show_media_previews(&self, room: &Room) -> bool {
272        match &*self.imp().media_previews_enabled.borrow() {
273            MediaPreviews::Off => false,
274            MediaPreviews::Private => matches!(
275                room.join_rule().value(),
276                JoinRuleValue::Invite | JoinRuleValue::RoomMembership
277            ),
278            _ => true,
279        }
280    }
281
282    /// Set which rooms display media previews.
283    pub(crate) async fn set_media_previews_enabled(
284        &self,
285        setting: MediaPreviews,
286    ) -> Result<(), ()> {
287        self.imp().set_media_previews_enabled(setting).await
288    }
289
290    /// Set whether to display avatars in invites.
291    pub(crate) async fn set_invite_avatars_enabled(&self, enabled: bool) -> Result<(), ()> {
292        self.imp().set_invite_avatars_enabled(enabled).await
293    }
294
295    /// Connect to the signal emitted when the media previews setting changed.
296    pub fn connect_media_previews_enabled_changed<F: Fn(&Self) + 'static>(
297        &self,
298        f: F,
299    ) -> glib::SignalHandlerId {
300        self.connect_closure(
301            "media-previews-enabled-changed",
302            true,
303            closure_local!(move |obj: Self| {
304                f(&obj);
305            }),
306        )
307    }
308}