fractal/session/view/content/
invite.rs

1use adw::subclass::prelude::*;
2use gettextrs::gettext;
3use gtk::{glib, glib::clone, prelude::*, CompositeTemplate};
4
5use crate::{
6    components::{
7        confirm_leave_room_dialog, Avatar, AvatarImageSafetySetting, LabelWithWidgets,
8        LoadingButton, Pill,
9    },
10    gettext_f,
11    prelude::*,
12    session::model::{MemberList, Room, RoomCategory, TargetRoomCategory, User},
13    toast,
14    utils::matrix::MatrixIdUri,
15};
16
17mod imp {
18    use std::{cell::RefCell, collections::HashSet};
19
20    use glib::subclass::InitializingObject;
21
22    use super::*;
23
24    #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
25    #[template(resource = "/org/gnome/Fractal/ui/session/view/content/invite.ui")]
26    #[properties(wrapper_type = super::Invite)]
27    pub struct Invite {
28        #[template_child]
29        pub(super) header_bar: TemplateChild<adw::HeaderBar>,
30        #[template_child]
31        avatar: TemplateChild<Avatar>,
32        #[template_child]
33        room_alias: TemplateChild<gtk::Label>,
34        #[template_child]
35        room_topic: TemplateChild<gtk::Label>,
36        #[template_child]
37        inviter: TemplateChild<LabelWithWidgets>,
38        #[template_child]
39        accept_button: TemplateChild<LoadingButton>,
40        #[template_child]
41        decline_button: TemplateChild<LoadingButton>,
42        /// The room currently displayed.
43        #[property(get, set = Self::set_room, explicit_notify, nullable)]
44        room: RefCell<Option<Room>>,
45        /// The list of members in the room.
46        room_members: RefCell<Option<MemberList>>,
47        /// The rooms that are currently being accepted.
48        accept_requests: RefCell<HashSet<Room>>,
49        /// The rooms that are currently being declined.
50        decline_requests: RefCell<HashSet<Room>>,
51        category_handler: RefCell<Option<glib::SignalHandlerId>>,
52    }
53
54    #[glib::object_subclass]
55    impl ObjectSubclass for Invite {
56        const NAME: &'static str = "ContentInvite";
57        type Type = super::Invite;
58        type ParentType = adw::Bin;
59
60        fn class_init(klass: &mut Self::Class) {
61            Self::bind_template(klass);
62            Self::bind_template_callbacks(klass);
63
64            klass.set_accessible_role(gtk::AccessibleRole::Group);
65        }
66
67        fn instance_init(obj: &InitializingObject<Self>) {
68            obj.init_template();
69        }
70    }
71
72    #[glib::derived_properties]
73    impl ObjectImpl for Invite {
74        fn constructed(&self) {
75            self.parent_constructed();
76            let obj = self.obj();
77
78            self.room_alias.connect_label_notify(|room_alias| {
79                room_alias.set_visible(!room_alias.label().is_empty());
80            });
81            self.room_alias
82                .set_visible(!self.room_alias.label().is_empty());
83
84            self.room_topic.connect_label_notify(|room_topic| {
85                room_topic.set_visible(!room_topic.label().is_empty());
86            });
87            self.room_topic
88                .set_visible(!self.room_topic.label().is_empty());
89            self.room_topic.connect_activate_link(clone!(
90                #[weak]
91                obj,
92                #[upgrade_or]
93                glib::Propagation::Proceed,
94                move |_, uri| {
95                    if MatrixIdUri::parse(uri).is_ok() {
96                        let _ =
97                            obj.activate_action("session.show-matrix-uri", Some(&uri.to_variant()));
98                        glib::Propagation::Stop
99                    } else {
100                        glib::Propagation::Proceed
101                    }
102                }
103            ));
104        }
105
106        fn dispose(&self) {
107            self.disconnect_signals();
108        }
109    }
110
111    impl WidgetImpl for Invite {
112        fn grab_focus(&self) -> bool {
113            self.accept_button.grab_focus()
114        }
115    }
116
117    impl BinImpl for Invite {}
118
119    #[gtk::template_callbacks]
120    impl Invite {
121        /// Set the room currently displayed.
122        fn set_room(&self, room: Option<Room>) {
123            if *self.room.borrow() == room {
124                return;
125            }
126
127            self.disconnect_signals();
128
129            match &room {
130                Some(room) if self.accept_requests.borrow().contains(room) => {
131                    self.decline_button.set_is_loading(false);
132                    self.decline_button.set_sensitive(false);
133                    self.accept_button.set_is_loading(true);
134                }
135                Some(room) if self.decline_requests.borrow().contains(room) => {
136                    self.accept_button.set_is_loading(false);
137                    self.accept_button.set_sensitive(false);
138                    self.decline_button.set_is_loading(true);
139                }
140                _ => self.reset(),
141            }
142
143            if let Some(room) = &room {
144                let category_handler = room.connect_category_notify(clone!(
145                    #[weak(rename_to = imp)]
146                    self,
147                    move |room| {
148                        let category = room.category();
149
150                        if category == RoomCategory::Left {
151                            // We declined the invite or the invite was retracted, we should close
152                            // the room if it is opened.
153                            let Some(session) = room.session() else {
154                                return;
155                            };
156                            let selection = session.sidebar_list_model().selection_model();
157                            if let Some(selected_room) =
158                                selection.selected_item().and_downcast::<Room>()
159                            {
160                                if selected_room == *room {
161                                    selection.set_selected_item(None::<glib::Object>);
162                                }
163                            }
164                        }
165
166                        if category != RoomCategory::Invited {
167                            imp.decline_requests.borrow_mut().remove(room);
168                            imp.accept_requests.borrow_mut().remove(room);
169                            imp.reset();
170
171                            if let Some(category_handler) = imp.category_handler.take() {
172                                room.disconnect(category_handler);
173                            }
174                        }
175                    }
176                ));
177                self.category_handler.replace(Some(category_handler));
178
179                if let Some(inviter) = room.inviter() {
180                    let pill = Pill::new(
181                        &inviter,
182                        AvatarImageSafetySetting::InviteAvatars,
183                        Some(room.clone()),
184                    );
185
186                    let label = gettext_f(
187                        // Translators: Do NOT translate the content between '{' and '}', these
188                        // are variable names.
189                        "{user_name} ({user_id}) invited you",
190                        &[
191                            ("user_name", LabelWithWidgets::PLACEHOLDER),
192                            ("user_id", inviter.user_id().as_str()),
193                        ],
194                    );
195
196                    self.inviter
197                        .set_label_and_widgets(label, vec![pill.clone()]);
198                }
199            }
200
201            // Keep a strong reference to the members list.
202            self.room_members
203                .replace(room.as_ref().map(Room::get_or_create_members));
204            self.room.replace(room);
205
206            self.obj().notify_room();
207        }
208
209        /// Reset the state of the view.
210        fn reset(&self) {
211            self.accept_button.set_is_loading(false);
212            self.accept_button.set_sensitive(true);
213
214            self.decline_button.set_is_loading(false);
215            self.decline_button.set_sensitive(true);
216        }
217
218        /// Accept the invite.
219        #[template_callback]
220        async fn accept(&self) {
221            let Some(room) = self.room.borrow().clone() else {
222                return;
223            };
224
225            self.decline_button.set_sensitive(false);
226            self.accept_button.set_is_loading(true);
227            self.accept_requests.borrow_mut().insert(room.clone());
228
229            if room
230                .change_category(TargetRoomCategory::Normal)
231                .await
232                .is_err()
233            {
234                toast!(
235                    self.obj(),
236                    gettext(
237                        // Translators: Do NOT translate the content between '{' and '}', this
238                        // is a variable name.
239                        "Could not accept invitation for {room}",
240                    ),
241                    @room,
242                );
243
244                self.accept_requests.borrow_mut().remove(&room);
245                self.reset();
246            }
247        }
248
249        /// Decline the invite.
250        #[template_callback]
251        async fn decline(&self) {
252            let Some(room) = self.room.borrow().clone() else {
253                return;
254            };
255
256            let obj = self.obj();
257
258            let Some(response) = confirm_leave_room_dialog(&room, &*obj).await else {
259                return;
260            };
261
262            self.accept_button.set_sensitive(false);
263            self.decline_button.set_is_loading(true);
264            self.decline_requests.borrow_mut().insert(room.clone());
265
266            let ignored_inviter = response.ignore_inviter.then(|| room.inviter()).flatten();
267
268            let closed = if room.change_category(TargetRoomCategory::Left).await.is_ok() {
269                // A room where we were invited is usually empty so just close it.
270                let _ = obj.activate_action("session.close-room", None);
271                true
272            } else {
273                toast!(
274                    obj,
275                    gettext(
276                        // Translators: Do NOT translate the content between '{' and '}', this
277                        // is a variable name.
278                        "Could not decline invitation for {room}",
279                    ),
280                    @room,
281                );
282
283                self.decline_requests.borrow_mut().remove(&room);
284                self.reset();
285                false
286            };
287
288            if let Some(inviter) = ignored_inviter {
289                if inviter.upcast::<User>().ignore().await.is_err() {
290                    toast!(obj, gettext("Could not ignore user"));
291                } else if !closed {
292                    // Ignoring the user should remove the room from the sidebar so close it.
293                    let _ = obj.activate_action("session.close-room", None);
294                }
295            }
296        }
297
298        /// Disconnect the signal handlers of this view.
299        fn disconnect_signals(&self) {
300            if let Some(room) = self.room.take() {
301                if let Some(handler) = self.category_handler.take() {
302                    room.disconnect(handler);
303                }
304            }
305        }
306    }
307}
308
309glib::wrapper! {
310    /// A view presenting an invitation to a room.
311    pub struct Invite(ObjectSubclass<imp::Invite>)
312        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
313}
314
315impl Invite {
316    pub fn new() -> Self {
317        glib::Object::new()
318    }
319
320    /// The header bar of the invite.
321    pub fn header_bar(&self) -> &adw::HeaderBar {
322        &self.imp().header_bar
323    }
324}