1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
use std::{cell::RefCell, fmt, rc::Rc};

use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{gio, glib, glib::clone};
use tracing::{debug, error, info, warn};

use crate::{
    config, intent,
    session::model::{Session, SessionState},
    session_list::{FailedSession, SessionInfo, SessionList},
    spawn,
    system_settings::SystemSettings,
    toast,
    utils::{matrix::MatrixIdUri, BoundObjectWeakRef, LoadingState},
    Window,
};

mod imp {
    use super::*;

    #[derive(Debug)]
    pub struct Application {
        /// The application settings.
        pub settings: gio::Settings,
        /// The system settings.
        pub system_settings: SystemSettings,
        /// The list of logged-in sessions.
        pub session_list: SessionList,
        pub intent_handler: BoundObjectWeakRef<glib::Object>,
    }

    impl Default for Application {
        fn default() -> Self {
            Self {
                settings: gio::Settings::new(config::APP_ID),
                system_settings: Default::default(),
                session_list: Default::default(),
                intent_handler: Default::default(),
            }
        }
    }

    #[glib::object_subclass]
    impl ObjectSubclass for Application {
        const NAME: &'static str = "Application";
        type Type = super::Application;
        type ParentType = adw::Application;
    }

    impl ObjectImpl for Application {
        fn constructed(&self) {
            self.parent_constructed();

            let app = self.obj();
            app.set_up_gactions();
            app.set_up_accels();

            self.session_list
                .connect_error_notify(clone!(@weak app => move |session_list| {
                    if let Some(message) = session_list.error() {
                        let window = app.present_main_window();
                        window.show_secret_error(&message);
                    }
                }));
            spawn!(
                clone!(@weak self.session_list as session_list => async move {
                    session_list.restore_sessions().await;
                })
            );
        }
    }

    impl ApplicationImpl for Application {
        fn activate(&self) {
            debug!("Application::activate");

            self.obj().present_main_window();
        }

        fn open(&self, files: &[gio::File], _hint: &str) {
            debug!("Application::open");

            self.obj().present_main_window();

            if files.len() > 1 {
                warn!("Trying to open several URIs, only the first one will be processed");
            }

            if let Some(uri) = files.first().map(|f| f.uri()) {
                self.obj().process_uri(&uri);
            } else {
                debug!("No URI to open");
            }
        }
    }

    impl GtkApplicationImpl for Application {}
    impl AdwApplicationImpl for Application {}
}

glib::wrapper! {
    pub struct Application(ObjectSubclass<imp::Application>)
        @extends gio::Application, gtk::Application, adw::Application,
        @implements gio::ActionMap, gio::ActionGroup;
}

impl Application {
    pub fn new() -> Self {
        glib::Object::builder()
            .property("application-id", Some(config::APP_ID))
            .property("flags", gio::ApplicationFlags::HANDLES_OPEN)
            .property("resource-base-path", Some("/org/gnome/Fractal/"))
            .build()
    }

    /// Get or create the main window and make sure it is visible.
    ///
    /// Returns the main window.
    fn present_main_window(&self) -> Window {
        let window = if let Some(window) = self.active_window().and_downcast() {
            window
        } else {
            Window::new(self)
        };

        window.present();
        window
    }

    /// The application settings.
    pub fn settings(&self) -> gio::Settings {
        self.imp().settings.clone()
    }

    /// The system settings.
    pub fn system_settings(&self) -> SystemSettings {
        self.imp().system_settings.clone()
    }

    /// The list of logged-in sessions.
    pub fn session_list(&self) -> &SessionList {
        &self.imp().session_list
    }

    /// Set up the application actions.
    fn set_up_gactions(&self) {
        self.add_action_entries([
            // Quit
            gio::ActionEntry::builder("quit")
                .activate(|app: &Application, _, _| {
                    if let Some(window) = app.active_window() {
                        // This is needed to trigger the delete event
                        // and saving the window state
                        window.close();
                    }

                    app.quit();
                })
                .build(),
            // About
            gio::ActionEntry::builder("about")
                .activate(|app: &Application, _, _| {
                    app.show_about_dialog();
                })
                .build(),
            // Show a room. This is the action triggered when clicking a notification.
            gio::ActionEntry::builder("show-room")
                .parameter_type(Some(&intent::ShowRoomPayload::static_variant_type()))
                .activate(|app: &Application, _, v| {
                    let Some(payload) = v.and_then(|v| v.get::<intent::ShowRoomPayload>()) else {
                        error!("Triggered `show-room` action without the proper payload");
                        return;
                    };

                    app.process_intent(intent::SessionIntent::ShowRoom(payload));
                })
                .build(),
        ]);
    }

    /// Sets up keyboard shortcuts for application and window actions.
    fn set_up_accels(&self) {
        self.set_accels_for_action("app.quit", &["<Control>q"]);
        self.set_accels_for_action("win.show-help-overlay", &["<Control>question"]);
    }

    fn show_about_dialog(&self) {
        let dialog = adw::AboutDialog::builder()
            .application_name("Fractal")
            .application_icon(config::APP_ID)
            .developer_name(gettext("The Fractal Team"))
            .license_type(gtk::License::Gpl30)
            .website("https://gitlab.gnome.org/World/fractal/")
            .issue_url("https://gitlab.gnome.org/World/fractal/-/issues")
            .support_url("https://matrix.to/#/#fractal:gnome.org")
            .version(config::VERSION)
            .copyright(gettext("© 2017-2024 The Fractal Team"))
            .developers(vec![
                "Alejandro Domínguez".to_string(),
                "Alexandre Franke".to_string(),
                "Bilal Elmoussaoui".to_string(),
                "Christopher Davis".to_string(),
                "Daniel García Moreno".to_string(),
                "Eisha Chen-yen-su".to_string(),
                "Jordan Petridis".to_string(),
                "Julian Sparber".to_string(),
                "Kévin Commaille".to_string(),
                "Saurav Sachidanand".to_string(),
            ])
            .designers(vec!["Tobias Bernard".to_string()])
            .translator_credits(gettext("translator-credits"))
            .build();

        // This can't be added via the builder
        dialog.add_credit_section(Some(&gettext("Name by")), &["Regina Bíró"]);

        // If the user wants our support room, try to open it ourselves.
        dialog.connect_activate_link(clone!(@weak self as obj, @weak dialog => @default-return false, move |_, uri| {
            if uri == "https://matrix.to/#/#fractal:gnome.org" && obj.session_list().has_session_ready() {
                obj.process_uri(uri);
                dialog.close();
                return true;
            }

            false
        }));

        dialog.present(&self.present_main_window());
    }

    /// Process the given URI.
    fn process_uri(&self, uri: &str) {
        match MatrixIdUri::parse(uri) {
            Ok(matrix_id) => self.process_intent(matrix_id),
            Err(error) => warn!("Invalid Matrix URI: {error}"),
        }
    }

    /// Process the given intent, as soon as possible.
    fn process_intent(&self, intent: impl Into<intent::AppIntent>) {
        let intent = intent.into();
        debug!("Processing intent {intent:?}");

        // We only handle a single intent at time, the latest one.
        self.imp().intent_handler.disconnect_signals();

        let session_list = self.session_list();

        if session_list.state() == LoadingState::Ready {
            match intent {
                intent::AppIntent::WithSession(session_intent) => {
                    self.process_session_intent(session_intent);
                }
                intent::AppIntent::ShowMatrixId(matrix_uri) => match session_list.n_items() {
                    0 => {
                        warn!("Cannot open URI with no logged in session");
                    }
                    1 => {
                        let session = session_list.first().expect("There should be one session");
                        let session_intent = intent::SessionIntent::with_matrix_uri(
                            session.session_id(),
                            matrix_uri,
                        );
                        self.process_session_intent(session_intent);
                    }
                    _ => {
                        spawn!(clone!(@weak self as obj => async move {
                            obj.choose_session_for_uri(matrix_uri).await;
                        }));
                    }
                },
            }
        } else {
            // Wait for the list to be ready.
            let cell = Rc::new(RefCell::new(Some(intent)));
            let handler = session_list.connect_state_notify(
                clone!(@weak self as app, @strong cell => move |session_list| {
                    if session_list.state() == LoadingState::Ready {
                        app.imp().intent_handler.disconnect_signals();

                        if let Some(intent) = cell.take() {
                            app.process_intent(intent);
                        }
                    }
                }),
            );
            self.imp()
                .intent_handler
                .set(session_list.upcast_ref(), vec![handler]);
        }
    }

    /// Ask the user to choose a session to process the given Matrix ID URI.
    ///
    /// The session list needs to be ready.
    async fn choose_session_for_uri(&self, matrix_uri: MatrixIdUri) {
        let main_window = self.present_main_window();

        let Some(session_id) = main_window.choose_session_for_uri().await else {
            warn!("No session selected to show URI");
            return;
        };

        let session_intent = intent::SessionIntent::with_matrix_uri(session_id, matrix_uri);
        self.process_session_intent(session_intent);
    }

    /// Process the given for a session, as soon as the session is ready.
    fn process_session_intent(&self, intent: intent::SessionIntent) {
        let Some(session_info) = self.session_list().get(intent.session_id()) else {
            warn!("Could not find session to process intent {intent:?}");
            toast!(self.present_main_window(), gettext("Session not found"));
            return;
        };
        if session_info.is::<FailedSession>() {
            // We can't do anything, it should show an error screen.
            warn!("Could not process intent {intent:?} for failed session");
        } else if let Some(session) = session_info.downcast_ref::<Session>() {
            if session.state() == SessionState::Ready {
                self.present_main_window()
                    .process_session_intent_ready(intent);
            } else {
                // Wait for the session to be ready.
                let cell = Rc::new(RefCell::new(Some(intent)));
                let handler = session.connect_state_notify(
                    clone!(@weak self as app, @strong cell => move |session| {
                        if session.state() == SessionState::Ready {
                            app.imp().intent_handler.disconnect_signals();

                            if let Some(intent) = cell.take() {
                                app.present_main_window().process_session_intent_ready(intent);
                            }
                        }
                    }),
                );
                self.imp()
                    .intent_handler
                    .set(session.upcast_ref(), vec![handler]);
            }
        } else {
            // Wait for the session to be a `Session`.
            let session_list = self.session_list();
            let cell = Rc::new(RefCell::new(Some(intent)));
            let handler = session_list.connect_items_changed(
                clone!(@weak self as app, @strong cell => move |session_list, pos, _, added| {
                    if added == 0 {
                        return;
                    }
                    let Some(session_id) = cell.borrow().as_ref().map(|i| i.session_id().to_owned()) else {
                        return;
                    };

                    for i in pos..pos + added {
                        let Some(session_info) = session_list.item(i).and_downcast::<SessionInfo>() else {
                            break;
                        };

                        if session_info.session_id() == session_id {
                            app.imp().intent_handler.disconnect_signals();

                            if let Some(intent) = cell.take() {
                                app.process_session_intent(intent);
                            }
                            break;
                        }
                    }
                }),
            );
            self.imp()
                .intent_handler
                .set(session_list.upcast_ref(), vec![handler]);
        }
    }

    pub fn run(&self) {
        info!("Fractal ({})", config::APP_ID);
        info!("Version: {} ({})", config::VERSION, config::PROFILE);
        info!("Datadir: {}", config::PKGDATADIR);

        ApplicationExtManual::run(self);
    }
}

impl Default for Application {
    fn default() -> Self {
        gio::Application::default()
            .and_downcast::<Application>()
            .unwrap()
    }
}

/// The profile that was built.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum AppProfile {
    /// A stable release.
    Stable,
    /// A beta release.
    Beta,
    /// A development release.
    Devel,
}

impl AppProfile {
    /// The string representation of this `AppProfile`.
    pub fn as_str(&self) -> &str {
        match self {
            Self::Stable => "stable",
            Self::Beta => "beta",
            Self::Devel => "devel",
        }
    }

    /// Whether this `AppProfile` should use the `.devel` CSS class on windows.
    pub fn should_use_devel_class(&self) -> bool {
        matches!(self, Self::Devel)
    }
}

impl fmt::Display for AppProfile {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}