fractal/session/view/content/room_details/
room_upgrade_dialog.rs

1use std::{cmp::Ordering, str::FromStr};
2
3use adw::prelude::*;
4use gettextrs::gettext;
5use gtk::{gio, glib, pango, subclass::prelude::*};
6use ruma::{
7    api::client::discovery::get_capabilities::{RoomVersionStability, RoomVersionsCapability},
8    RoomVersionId,
9};
10use tracing::error;
11
12/// Show a dialog to confirm the room upgrade and select a room version.
13///
14/// Returns the selected room version, or `None` if the user didn't confirm.
15pub(crate) async fn confirm_room_upgrade(
16    capability: RoomVersionsCapability,
17    parent: &impl IsA<gtk::Widget>,
18) -> Option<RoomVersionId> {
19    // Build the lists.
20    let default = capability.default;
21    let (mut stable_list, mut experimental_list) = capability
22        .available
23        .into_iter()
24        .map(|(id, stability)| {
25            let stability = if id == default {
26                // According to the spec, the default version is always assumed to be stable.
27                RoomVersionStability::Stable
28            } else {
29                stability
30            };
31
32            RoomVersion::new(id, stability)
33        })
34        .partition::<Vec<_>, _>(|version| *version.stability() == RoomVersionStability::Stable);
35
36    stable_list.sort_unstable_by(RoomVersion::cmp_ids);
37    experimental_list.sort_unstable_by(RoomVersion::cmp_ids);
38
39    let default_pos = stable_list
40        .iter()
41        .position(|v| *v.id() == default)
42        .unwrap_or_default();
43
44    // Construct the list models for the combo row.
45    let stable_model = stable_list.into_iter().collect::<gio::ListStore>();
46    let experimental_model = experimental_list.into_iter().collect::<gio::ListStore>();
47
48    let model_list = gio::ListStore::new::<gio::ListStore>();
49    model_list.append(&stable_model);
50    model_list.append(&experimental_model);
51    let flatten_model = gtk::FlattenListModel::new(Some(model_list));
52
53    // Construct the header factory to separate stable from experimental versions.
54    let header_factory = gtk::SignalListItemFactory::new();
55    header_factory.connect_setup(|_, header| {
56        let Some(header) = header.downcast_ref::<gtk::ListHeader>() else {
57            error!("List item factory did not receive a list header: {header:?}");
58            return;
59        };
60
61        let label = gtk::Label::builder()
62            .margin_start(12)
63            .xalign(0.0)
64            .ellipsize(pango::EllipsizeMode::End)
65            .css_classes(["heading"])
66            .build();
67        header.set_child(Some(&label));
68    });
69    header_factory.connect_bind(|_, header| {
70        let Some(header) = header.downcast_ref::<gtk::ListHeader>() else {
71            error!("List item factory did not receive a list header: {header:?}");
72            return;
73        };
74        let Some(label) = header.child().and_downcast::<gtk::Label>() else {
75            error!("List header does not have a child GtkLabel");
76            return;
77        };
78        let Some(version) = header.item().and_downcast::<RoomVersion>() else {
79            error!("List header does not have a RoomVersion item");
80            return;
81        };
82
83        let text = match version.stability() {
84            // Translators: As in 'Stable version'.
85            RoomVersionStability::Stable => gettext("Stable"),
86            // Translators: As in 'Experimental version'.
87            _ => gettext("Experimental"),
88        };
89        label.set_label(&text);
90    });
91
92    // Add an entry for the optional reason.
93    let version_combo = adw::ComboRow::builder()
94        .title(gettext("Version"))
95        .selectable(false)
96        .expression(RoomVersion::this_expression("id-string"))
97        .header_factory(&header_factory)
98        .model(&flatten_model)
99        .selected(default_pos.try_into().unwrap_or(u32::MAX))
100        .build();
101    let list_box = gtk::ListBox::builder()
102        .css_classes(["boxed-list"])
103        .margin_top(6)
104        .accessible_role(gtk::AccessibleRole::Group)
105        .build();
106    list_box.append(&version_combo);
107
108    // Build dialog.
109    let upgrade_dialog = adw::AlertDialog::builder()
110        .default_response("cancel")
111        .heading(gettext("Upgrade Room"))
112        .body(gettext("Upgrading a room to a more recent version allows to benefit from new features from the Matrix specification. It can also be used to reset the room state, which should make the room faster to join. However it should be used sparingly because it can be disruptive, as room members need to join the new room manually."))
113        .extra_child(&list_box)
114        .build();
115    upgrade_dialog.add_responses(&[
116        ("cancel", &gettext("Cancel")),
117        // Translators: In this string, 'Upgrade' is a verb, as in 'Upgrade Room'.
118        ("upgrade", &gettext("Upgrade")),
119    ]);
120    upgrade_dialog.set_response_appearance("upgrade", adw::ResponseAppearance::Destructive);
121
122    if upgrade_dialog.choose_future(parent).await != "upgrade" {
123        return None;
124    }
125
126    version_combo
127        .selected_item()
128        .and_downcast::<RoomVersion>()
129        .map(|v| v.id().clone())
130}
131
132mod imp {
133    use std::{cell::OnceCell, marker::PhantomData};
134
135    use super::*;
136
137    #[derive(Debug, Default, glib::Properties)]
138    #[properties(wrapper_type = super::RoomVersion)]
139    pub struct RoomVersion {
140        /// The ID of the version.
141        id: OnceCell<RoomVersionId>,
142        /// The ID of the version as a string.
143        #[property(get = Self::id_string)]
144        id_string: PhantomData<String>,
145        /// The stability of the version.
146        stability: OnceCell<RoomVersionStability>,
147    }
148
149    #[glib::object_subclass]
150    impl ObjectSubclass for RoomVersion {
151        const NAME: &'static str = "RoomUpgradeDialogRoomVersion";
152        type Type = super::RoomVersion;
153    }
154
155    #[glib::derived_properties]
156    impl ObjectImpl for RoomVersion {}
157
158    impl RoomVersion {
159        /// Set the ID of this version.
160        pub(super) fn set_id(&self, id: RoomVersionId) {
161            self.id.set(id).expect("id is uninitialized");
162        }
163
164        /// The ID of this version.
165        pub(super) fn id(&self) -> &RoomVersionId {
166            self.id.get().expect("id is initialized")
167        }
168
169        /// The ID of this version as a string.
170        fn id_string(&self) -> String {
171            self.id().to_string()
172        }
173
174        /// Set the stability of this version.
175        pub(super) fn set_stability(&self, stability: RoomVersionStability) {
176            self.stability
177                .set(stability)
178                .expect("stability is uninitialized");
179        }
180
181        /// The stability of this version.
182        pub(super) fn stability(&self) -> &RoomVersionStability {
183            self.stability.get().expect("stability is initialized")
184        }
185    }
186}
187
188glib::wrapper! {
189    /// A room version.
190    pub struct RoomVersion(ObjectSubclass<imp::RoomVersion>);
191}
192
193impl RoomVersion {
194    /// Constructs a new `RoomVersion`.
195    pub fn new(id: RoomVersionId, stability: RoomVersionStability) -> Self {
196        let obj = glib::Object::new::<Self>();
197
198        let imp = obj.imp();
199        imp.set_id(id);
200        imp.set_stability(stability);
201
202        obj
203    }
204
205    /// The ID of this version.
206    pub(crate) fn id(&self) -> &RoomVersionId {
207        self.imp().id()
208    }
209
210    /// The stability of this version.
211    pub(crate) fn stability(&self) -> &RoomVersionStability {
212        self.imp().stability()
213    }
214
215    /// Compare the IDs of the two given `RoomVersion`s.
216    ///
217    /// Correctly sorts numbers: string comparison will sort `1, 10, 2`, we want
218    /// `1, 2, 10`.
219    fn cmp_ids(a: &RoomVersion, b: &RoomVersion) -> Ordering {
220        match (
221            i64::from_str(a.id().as_str()),
222            i64::from_str(b.id().as_str()),
223        ) {
224            (Ok(a), Ok(b)) => a.cmp(&b),
225            (Ok(_), _) => Ordering::Less,
226            (_, Ok(_)) => Ordering::Greater,
227            _ => a.id().cmp(b.id()),
228        }
229    }
230}