fractal/session/view/content/
invite.rs1use 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 #[property(get, set = Self::set_room, explicit_notify, nullable)]
44 room: RefCell<Option<Room>>,
45 room_members: RefCell<Option<MemberList>>,
47 accept_requests: RefCell<HashSet<Room>>,
49 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 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 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 "{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 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 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 #[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 "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 #[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 let _ = obj.activate_action("session.close-room", None);
271 true
272 } else {
273 toast!(
274 obj,
275 gettext(
276 "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 let _ = obj.activate_action("session.close-room", None);
294 }
295 }
296 }
297
298 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 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 pub fn header_bar(&self) -> &adw::HeaderBar {
322 &self.imp().header_bar
323 }
324}