1use adw::prelude::*;
4use gettextrs::gettext;
5
6use crate::{
7 i18n::gettext_f,
8 ngettext_f,
9 prelude::*,
10 session::{Member, Membership, Room, RoomCategory, User},
11};
12
13pub(crate) async fn confirm_leave_room_dialog(
19 room: &Room,
20 parent: &impl IsA<gtk::Widget>,
21) -> Option<ConfirmLeaveRoomResponse> {
22 let (heading, body, response) = if room.category() == RoomCategory::Invited {
23 let heading = gettext("Decline Invite?");
25 let body = if room.join_rule().we_can_join() {
26 gettext(
27 "Do you really want to decline this invite? You can join this room on your own later.",
28 )
29 } else {
30 gettext(
31 "Do you really want to decline this invite? You will not be able to join this room without it.",
32 )
33 };
34 let response = gettext("Decline");
35
36 (heading, body, response)
37 } else {
38 let heading = gettext("Leave Room?");
40 let body = if room.join_rule().we_can_join() {
41 gettext("Do you really want to leave this room? You can come back later.")
42 } else {
43 gettext(
44 "Do you really want to leave this room? You will not be able to come back without an invitation.",
45 )
46 };
47 let response = gettext("Leave");
48
49 (heading, body, response)
50 };
51
52 let confirm_dialog = adw::AlertDialog::builder()
54 .default_response("cancel")
55 .heading(heading)
56 .body(body)
57 .build();
58 confirm_dialog.add_responses(&[("cancel", &gettext("Cancel")), ("leave", &response)]);
59 confirm_dialog.set_response_appearance("leave", adw::ResponseAppearance::Destructive);
60
61 let ignore_inviter_switch = if let Some(inviter) = room
62 .inviter()
63 .filter(|_| room.category() == RoomCategory::Invited)
64 {
65 let switch = adw::SwitchRow::builder()
66 .title(gettext_f(
67 "Ignore {user}",
68 &[("user", inviter.user_id().as_str())],
69 ))
70 .subtitle(gettext(
71 "All messages or invitations sent by this user will be ignored",
72 ))
73 .build();
74
75 let list_box = gtk::ListBox::builder()
76 .css_classes(["boxed-list"])
77 .margin_top(6)
78 .accessible_role(gtk::AccessibleRole::Group)
79 .build();
80 list_box.append(&switch);
81 confirm_dialog.set_extra_child(Some(&list_box));
82
83 Some(switch)
84 } else {
85 None
86 };
87
88 if confirm_dialog.choose_future(Some(parent)).await == "leave" {
89 let mut response = ConfirmLeaveRoomResponse::default();
90
91 if let Some(switch) = ignore_inviter_switch {
92 response.ignore_inviter = switch.is_active();
93 }
94
95 Some(response)
96 } else {
97 None
98 }
99}
100
101#[derive(Debug, Default, Clone)]
103pub(crate) struct ConfirmLeaveRoomResponse {
104 pub ignore_inviter: bool,
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub(crate) enum RoomMemberDestructiveAction {
111 Ban(usize),
115 Kick,
117 RemoveMessages(usize),
121}
122
123impl RoomMemberDestructiveAction {
124 fn dialog_content(self, member: &Member) -> (String, String, Option<String>) {
128 match self {
129 RoomMemberDestructiveAction::Ban(_) => {
130 let heading = gettext_f("Ban {user}?", &[("user", &member.display_name())]);
133 let body = gettext_f(
134 "Are you sure you want to ban {user_id}? They will not be able to join the room again until someone unbans them.",
137 &[("user_id", member.user_id().as_str())],
138 );
139 let response = gettext("Ban");
140 (heading, body, Some(response))
141 }
142 RoomMemberDestructiveAction::Kick => {
143 let can_rejoin = member.room().join_rule().anyone_can_join();
144
145 match member.membership() {
146 Membership::Invite => {
147 let heading = gettext_f(
148 "Revoke Invite for {user}?",
151 &[("user", &member.display_name())],
152 );
153 let body = if can_rejoin {
154 gettext_f(
155 "Are you sure you want to revoke the invite for {user_id}? They will still be able to join the room on their own.",
158 &[("user_id", member.user_id().as_str())],
159 )
160 } else {
161 gettext_f(
162 "Are you sure you want to revoke the invite for {user_id}? They will not be able to join the room again until someone reinvites them.",
165 &[("user_id", member.user_id().as_str())],
166 )
167 };
168 let response = gettext("Revoke Invite");
169 (heading, body, Some(response))
170 }
171 Membership::Knock => {
172 let heading = gettext_f(
173 "Deny Access to {user}?",
176 &[("user", &member.display_name())],
177 );
178 let body = gettext_f(
179 "Are you sure you want to deny access to {user_id}?",
182 &[("user_id", member.user_id().as_str())],
183 );
184 let response = gettext("Deny Access");
185 (heading, body, Some(response))
186 }
187 _ => {
188 let heading =
191 gettext_f("Kick {user}?", &[("user", &member.display_name())]);
192 let body = if can_rejoin {
193 gettext_f(
194 "Are you sure you want to kick {user_id}? They will still be able to join the room again on their own.",
197 &[("user_id", member.user_id().as_str())],
198 )
199 } else {
200 gettext_f(
201 "Are you sure you want to kick {user_id}? They will not be able to join the room again until someone invites them.",
204 &[("user_id", member.user_id().as_str())],
205 )
206 };
207 let response = gettext("Kick");
208 (heading, body, Some(response))
209 }
210 }
211 }
212 RoomMemberDestructiveAction::RemoveMessages(count) => {
213 let n = u32::try_from(count).unwrap_or(u32::MAX);
214 if count > 0 {
215 let heading = gettext_f(
216 "Remove Messages Sent by {user}?",
219 &[("user", &member.display_name())],
220 );
221 let body = ngettext_f(
222 "This removes all the messages received from the homeserver. Are you sure you want to remove 1 message sent by {user_id}? This cannot be undone.",
225 "This removes all the messages received from the homeserver. Are you sure you want to remove {n} messages sent by {user_id}? This cannot be undone.",
226 n,
227 &[
228 ("n", &n.to_string()),
229 ("user_id", member.user_id().as_str()),
230 ],
231 );
232 let response = gettext("Remove");
233 (heading, body, Some(response))
234 } else {
235 let heading = gettext_f(
236 "No Messages Sent by {user}",
239 &[("user", &member.display_name())],
240 );
241 let body = gettext_f(
242 "There are no messages received from the homeserver sent by {user_id}. You can try to load more by going further back in the room history.",
245 &[("user_id", member.user_id().as_str())],
246 );
247 (heading, body, None)
248 }
249 }
250 }
251 }
252}
253
254pub(crate) async fn confirm_room_member_destructive_action_dialog(
259 member: &Member,
260 action: RoomMemberDestructiveAction,
261 parent: &impl IsA<gtk::Widget>,
262) -> Option<ConfirmRoomMemberDestructiveActionResponse> {
263 let (heading, body, response) = action.dialog_content(member);
264
265 let child = gtk::Box::builder()
266 .orientation(gtk::Orientation::Vertical)
267 .spacing(12)
268 .build();
269
270 let reason_entry = adw::EntryRow::builder()
272 .title(gettext("Reason (optional)"))
273 .build();
274 let list_box = gtk::ListBox::builder()
275 .css_classes(["boxed-list"])
276 .margin_top(6)
277 .accessible_role(gtk::AccessibleRole::Group)
278 .build();
279 list_box.append(&reason_entry);
280 child.append(&list_box);
281
282 let removable_events_count = if let RoomMemberDestructiveAction::Ban(count) = action {
285 count
286 } else {
287 0
288 };
289
290 let remove_events_switch = if removable_events_count > 0 {
291 let n = u32::try_from(removable_events_count).unwrap_or(u32::MAX);
292 let switch = adw::SwitchRow::builder()
293 .title(ngettext_f(
294 "Remove the latest message sent by the user",
297 "Remove the {n} latest messages sent by the user",
298 n,
299 &[("n", &n.to_string())],
300 ))
301 .build();
302
303 let list_box = gtk::ListBox::builder()
304 .css_classes(["boxed-list"])
305 .margin_top(6)
306 .accessible_role(gtk::AccessibleRole::Group)
307 .build();
308 list_box.append(&switch);
309 child.append(&list_box);
310
311 Some(switch)
312 } else {
313 None
314 };
315
316 let confirm_dialog = adw::AlertDialog::builder()
318 .default_response("cancel")
319 .heading(heading)
320 .body(body)
321 .extra_child(&child)
322 .build();
323 confirm_dialog.add_responses(&[("cancel", &gettext("Cancel"))]);
324
325 if let Some(response) = response {
326 confirm_dialog.add_responses(&[("confirm", &response)]);
327 confirm_dialog.set_response_appearance("confirm", adw::ResponseAppearance::Destructive);
328 }
329
330 if confirm_dialog.choose_future(Some(parent)).await != "confirm" {
331 return None;
332 }
333
334 let reason = Some(reason_entry.text().trim().to_owned()).filter(|s| !s.is_empty());
336
337 let mut response = ConfirmRoomMemberDestructiveActionResponse {
338 reason,
339 ..Default::default()
340 };
341
342 if let Some(switch) = remove_events_switch {
343 response.remove_events = switch.is_active();
344 }
345
346 Some(response)
347}
348
349#[derive(Debug, Default, Clone)]
352pub(crate) struct ConfirmRoomMemberDestructiveActionResponse {
353 pub reason: Option<String>,
355 pub remove_events: bool,
357}
358
359pub(crate) async fn confirm_mute_room_member_dialog(
361 members: &[impl IsA<User>],
362 parent: &impl IsA<gtk::Widget>,
363) -> bool {
364 if members.is_empty() {
365 return false;
366 }
367
368 let first_member = members
369 .first()
370 .expect("there should be at least one member")
371 .upcast_ref();
372 let is_single_member = members.len() == 1;
373
374 let heading = if is_single_member {
377 gettext_f(
378 "Mute {user}?",
381 &[("user", &first_member.display_name())],
382 )
383 } else {
384 gettext("Mute Members?")
385 };
386
387 let body = if is_single_member {
390 gettext_f(
391 "Are you sure you want to mute {user_id}? They will not be able to send new messages to this room.",
394 &[("user_id", first_member.user_id().as_str())],
395 )
396 } else {
397 gettext(
398 "Are you sure you want to mute these members? They will not be able to send new messages to this room.",
399 )
400 };
401
402 let confirm_dialog = adw::AlertDialog::builder()
404 .default_response("cancel")
405 .heading(heading)
406 .body(body)
407 .build();
408 confirm_dialog.add_responses(&[
409 ("cancel", &gettext("Cancel")),
410 ("mute", &gettext("Mute")),
412 ]);
413 confirm_dialog.set_response_appearance("mute", adw::ResponseAppearance::Destructive);
414
415 confirm_dialog.choose_future(Some(parent)).await == "mute"
416}
417
418pub(crate) async fn confirm_set_room_member_power_level_same_as_own_dialog(
421 members: &[impl IsA<User>],
422 parent: &impl IsA<gtk::Widget>,
423) -> bool {
424 if members.is_empty() {
425 return false;
426 }
427
428 let first_member = members
429 .first()
430 .expect("there should be at least one member")
431 .upcast_ref();
432 let is_single_member = members.len() == 1;
433
434 let heading = if is_single_member {
437 gettext_f(
438 "Promote {user}?",
441 &[("user", &first_member.display_name())],
442 )
443 } else {
444 gettext("Promote Members?")
445 };
446
447 let body = if is_single_member {
450 gettext_f(
451 "If you promote {user_id} to the same level as yours, you will not be able to demote them in the future.",
454 &[("user_id", first_member.user_id().as_str())],
455 )
456 } else {
457 gettext(
458 "If you promote these members to the same level as yours, you will not be able to demote them in the future.",
459 )
460 };
461
462 let confirm_dialog = adw::AlertDialog::builder()
464 .default_response("cancel")
465 .heading(heading)
466 .body(body)
467 .build();
468 confirm_dialog.add_responses(&[
469 ("cancel", &gettext("Cancel")),
470 ("promote", &gettext("Promote")),
471 ]);
472 confirm_dialog.set_response_appearance("promote", adw::ResponseAppearance::Destructive);
473
474 confirm_dialog.choose_future(Some(parent)).await == "promote"
475}
476
477pub(crate) async fn confirm_own_demotion_dialog(parent: &impl IsA<gtk::Widget>) -> bool {
479 let heading = gettext("Demote Yourself?");
480 let body = gettext(
481 "Are you sure you want to lower your power level? You will need to ask another member with a higher power level to undo this.",
482 );
483
484 let confirm_dialog = adw::AlertDialog::builder()
486 .default_response("cancel")
487 .heading(heading)
488 .body(body)
489 .build();
490 confirm_dialog.add_responses(&[
491 ("cancel", &gettext("Cancel")),
492 ("demote", &gettext("Demote")),
493 ]);
494 confirm_dialog.set_response_appearance("demote", adw::ResponseAppearance::Destructive);
495
496 confirm_dialog.choose_future(Some(parent)).await == "demote"
497}
498
499pub(crate) async fn unsaved_changes_dialog(
501 parent: &impl IsA<gtk::Widget>,
502) -> UnsavedChangesResponse {
503 let title = gettext("Save Changes?");
504 let description =
505 gettext("This page contains unsaved changes. Changes which are not saved will be lost.");
506 let dialog = adw::AlertDialog::builder()
507 .title(title)
508 .body(description)
509 .default_response("cancel")
510 .build();
511
512 dialog.add_responses(&[
513 ("cancel", &gettext("Cancel")),
514 ("discard", &gettext("Discard")),
515 ("save", &gettext("Save")),
516 ]);
517 dialog.set_response_appearance("discard", adw::ResponseAppearance::Destructive);
518 dialog.set_response_appearance("save", adw::ResponseAppearance::Suggested);
519
520 match dialog.choose_future(Some(parent)).await.as_str() {
521 "discard" => UnsavedChangesResponse::Discard,
522 "save" => UnsavedChangesResponse::Save,
523 _ => UnsavedChangesResponse::Cancel,
524 }
525}
526
527#[derive(Debug, Clone, Copy, PartialEq, Eq)]
529pub(crate) enum UnsavedChangesResponse {
530 Save,
532 Discard,
534 Cancel,
536}