You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
445 lines
17 KiB
445 lines
17 KiB
use std::cmp::Ordering; |
|
|
|
use adw::{prelude::*, subclass::prelude::*}; |
|
use gettextrs::{gettext, ngettext}; |
|
use gtk::{gio, glib, pango}; |
|
use ruma::{ |
|
OwnedUserId, RoomVersionId, |
|
api::client::discovery::get_capabilities::v3::{RoomVersionStability, RoomVersionsCapability}, |
|
}; |
|
use tracing::error; |
|
|
|
mod room_version; |
|
|
|
use self::room_version::RoomVersion; |
|
use crate::{session::JoinRuleValue, utils::OneshotNotifier}; |
|
|
|
mod imp { |
|
use std::cell::OnceCell; |
|
|
|
use glib::subclass::InitializingObject; |
|
|
|
use super::*; |
|
|
|
#[derive(Debug, Default, gtk::CompositeTemplate)] |
|
#[template(resource = "/org/gnome/Fractal/ui/session_view/room_details/upgrade_dialog/mod.ui")] |
|
pub struct UpgradeDialog { |
|
#[template_child] |
|
version_combo: TemplateChild<adw::ComboRow>, |
|
#[template_child] |
|
invite_only_warning_label: TemplateChild<gtk::Label>, |
|
#[template_child] |
|
creators_warning_label: TemplateChild<gtk::Label>, |
|
header_factory: OnceCell<gtk::SignalListItemFactory>, |
|
/// The notifier for the response of the user. |
|
notifier: OnceCell<OneshotNotifier<Option<RoomVersionId>>>, |
|
} |
|
|
|
#[glib::object_subclass] |
|
impl ObjectSubclass for UpgradeDialog { |
|
const NAME: &'static str = "RoomDetailsUpgradeDialog"; |
|
type Type = super::UpgradeDialog; |
|
type ParentType = adw::Dialog; |
|
|
|
fn class_init(klass: &mut Self::Class) { |
|
Self::bind_template(klass); |
|
Self::bind_template_callbacks(klass); |
|
} |
|
|
|
fn instance_init(obj: &InitializingObject<Self>) { |
|
obj.init_template(); |
|
} |
|
} |
|
|
|
impl ObjectImpl for UpgradeDialog { |
|
fn constructed(&self) { |
|
self.parent_constructed(); |
|
|
|
self.version_combo |
|
.set_expression(Some(RoomVersion::this_expression("id-string"))); |
|
} |
|
} |
|
|
|
impl WidgetImpl for UpgradeDialog {} |
|
|
|
impl AdwDialogImpl for UpgradeDialog { |
|
fn closed(&self) { |
|
if let Some(notifier) = self.notifier.get() { |
|
notifier.notify(); |
|
} |
|
} |
|
} |
|
|
|
#[gtk::template_callbacks] |
|
impl UpgradeDialog { |
|
/// The notifier for the response of the user. |
|
fn notifier(&self) -> &OneshotNotifier<Option<RoomVersionId>> { |
|
self.notifier |
|
.get_or_init(|| OneshotNotifier::new("UpgradeDialog")) |
|
} |
|
|
|
/// The header factory to separate stable from experimental versions. |
|
fn header_factory(&self) -> >k::SignalListItemFactory { |
|
self.header_factory.get_or_init(|| { |
|
let header_factory = gtk::SignalListItemFactory::new(); |
|
|
|
header_factory.connect_setup(|_, header| { |
|
let Some(header) = header.downcast_ref::<gtk::ListHeader>() else { |
|
error!("List item factory did not receive a list header: {header:?}"); |
|
return; |
|
}; |
|
|
|
let label = gtk::Label::builder() |
|
.margin_start(12) |
|
.xalign(0.0) |
|
.ellipsize(pango::EllipsizeMode::End) |
|
.css_classes(["heading"]) |
|
.build(); |
|
header.set_child(Some(&label)); |
|
}); |
|
header_factory.connect_bind(|_, header| { |
|
let Some(header) = header.downcast_ref::<gtk::ListHeader>() else { |
|
error!("List item factory did not receive a list header: {header:?}"); |
|
return; |
|
}; |
|
let Some(label) = header.child().and_downcast::<gtk::Label>() else { |
|
error!("List header does not have a child GtkLabel"); |
|
return; |
|
}; |
|
let Some(version) = header.item().and_downcast::<RoomVersion>() else { |
|
error!("List header does not have a RoomVersion item"); |
|
return; |
|
}; |
|
|
|
let text = if version.is_stable() { |
|
// Translators: As in 'Stable version'. |
|
gettext("Stable") |
|
} else { |
|
// Translators: As in 'Experimental version'. |
|
gettext("Experimental") |
|
}; |
|
label.set_label(&text); |
|
}); |
|
|
|
header_factory |
|
}) |
|
} |
|
|
|
/// Ask the user to confirm the room upgrade and select a room version |
|
/// among the ones that are supported by the server. |
|
/// |
|
/// Returns the selected room version, or `None` if the user cancelled |
|
/// the upgrade. |
|
pub(super) async fn confirm_upgrade( |
|
&self, |
|
info: &UpgradeInfo, |
|
parent: >k::Widget, |
|
) -> Option<RoomVersionId> { |
|
self.update_version_combo(info); |
|
self.update_invite_only_warning(info); |
|
self.update_creators_warning(info); |
|
|
|
let receiver = self.notifier().listen(); |
|
|
|
self.obj().present(Some(parent)); |
|
|
|
receiver.await |
|
} |
|
|
|
/// Update the room versions combo row with the given details. |
|
fn update_version_combo(&self, info: &UpgradeInfo) { |
|
// Construct the list models for the combo row. |
|
let stable_model = (!info.stable_room_versions.is_empty()).then(|| { |
|
info.stable_room_versions |
|
.iter() |
|
.map(|version| RoomVersion::new(version.clone(), true)) |
|
.collect::<gio::ListStore>() |
|
}); |
|
let unstable_model = (!info.unstable_room_versions.is_empty()).then(|| { |
|
info.unstable_room_versions |
|
.iter() |
|
.map(|version| RoomVersion::new(version.clone(), false)) |
|
.collect::<gio::ListStore>() |
|
}); |
|
|
|
let use_header_factory = unstable_model.is_some(); |
|
let model = match (stable_model, unstable_model) { |
|
(Some(model), None) | (None, Some(model)) => model.upcast::<gio::ListModel>(), |
|
(Some(stable_model), Some(unstable_model)) => { |
|
let model_list = gio::ListStore::new::<gio::ListStore>(); |
|
model_list.append(&stable_model); |
|
model_list.append(&unstable_model); |
|
gtk::FlattenListModel::new(Some(model_list)).upcast() |
|
} |
|
// We always have at least the current room version. |
|
(None, None) => unreachable!(), |
|
}; |
|
|
|
self.version_combo |
|
.set_header_factory(use_header_factory.then(|| self.header_factory())); |
|
self.version_combo.set_model(Some(&model)); |
|
self.version_combo |
|
.set_selected(info.selected.try_into().unwrap_or(u32::MAX)); |
|
} |
|
|
|
/// Update the invite-only warning. |
|
fn update_invite_only_warning(&self, info: &UpgradeInfo) { |
|
self.invite_only_warning_label |
|
.set_visible(info.join_rule == JoinRuleValue::Invite); |
|
} |
|
|
|
/// Update the creators warning. |
|
fn update_creators_warning(&self, info: &UpgradeInfo) { |
|
if info.other_creators_count == 0 { |
|
// We are not changing the list of privileged creators. |
|
self.creators_warning_label.set_visible(false); |
|
return; |
|
} |
|
|
|
let other_creators_count = u32::try_from(info.other_creators_count).unwrap_or(u32::MAX); |
|
|
|
let text = if info.own_user_is_creator { |
|
ngettext( |
|
"After the upgrade, you will be the only creator in the room. The other creator will be demoted to the default power level.", |
|
"After the upgrade, you will be the only creator in the room. The other creators will be demoted to the default power level.", |
|
other_creators_count, |
|
) |
|
} else { |
|
ngettext( |
|
"After the upgrade, you will be the only creator in the room. The current creator will be demoted to the default power level.", |
|
"After the upgrade, you will be the only creator in the room. The current creators will be demoted to the default power level.", |
|
other_creators_count, |
|
) |
|
}; |
|
|
|
self.creators_warning_label.set_label(&text); |
|
self.creators_warning_label.set_visible(true); |
|
} |
|
|
|
/// Confirm the upgrade. |
|
#[template_callback] |
|
fn upgrade(&self) { |
|
let room_version = self |
|
.version_combo |
|
.selected_item() |
|
.and_downcast::<RoomVersion>() |
|
.map(|v| v.id().clone()); |
|
|
|
self.notifier().notify_value(room_version); |
|
self.obj().close(); |
|
} |
|
|
|
/// Cancel the upgrade. |
|
#[template_callback] |
|
fn cancel(&self) { |
|
self.obj().close(); |
|
} |
|
} |
|
} |
|
|
|
glib::wrapper! { |
|
/// Dialog to confirm a room upgrade and select a room version. |
|
pub struct UpgradeDialog(ObjectSubclass<imp::UpgradeDialog>) |
|
@extends gtk::Widget, adw::Dialog, |
|
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::ShortcutManager; |
|
} |
|
|
|
impl UpgradeDialog { |
|
pub fn new() -> Self { |
|
glib::Object::new() |
|
} |
|
|
|
/// Ask the user to confirm the room upgrade and select a room version among |
|
/// the ones that are supported by the server. |
|
/// |
|
/// Returns the selected room version, or `None` if the user cancelled the |
|
/// upgrade. |
|
pub(crate) async fn confirm_upgrade( |
|
&self, |
|
info: &UpgradeInfo, |
|
parent: &impl IsA<gtk::Widget>, |
|
) -> Option<RoomVersionId> { |
|
self.imp().confirm_upgrade(info, parent.upcast_ref()).await |
|
} |
|
} |
|
|
|
/// The information necessary for [`UpgradeDialog`]. |
|
#[derive(Debug, Clone)] |
|
pub(crate) struct UpgradeInfo { |
|
/// The sorted stable room versions available for the upgrade. |
|
pub(crate) stable_room_versions: Vec<RoomVersionId>, |
|
/// The sorted unstable room versions available for the upgrade. |
|
pub(crate) unstable_room_versions: Vec<RoomVersionId>, |
|
/// The position of the room version that should be selected by default, |
|
/// when `stable_room_versions` and `unstable_room_versions` are |
|
/// concatenated. |
|
pub(crate) selected: usize, |
|
/// Whether our own user is a privileged creator in the current room. |
|
pub(crate) own_user_is_creator: bool, |
|
/// The number of privileged creators that are not our own user in the |
|
/// current room. |
|
pub(crate) other_creators_count: usize, |
|
/// The current join rule of the room. |
|
pub(crate) join_rule: JoinRuleValue, |
|
} |
|
|
|
impl UpgradeInfo { |
|
/// Construct an empty `UpgradeInfo`. |
|
pub(crate) fn new(join_rule: JoinRuleValue) -> Self { |
|
Self { |
|
stable_room_versions: vec![], |
|
unstable_room_versions: vec![], |
|
selected: 0, |
|
own_user_is_creator: false, |
|
other_creators_count: 0, |
|
join_rule, |
|
} |
|
} |
|
|
|
/// Add information about the possible room versions for the upgrade. |
|
/// |
|
/// We do not allow users to: |
|
/// |
|
/// - Downgrade the room, i.e. use a lower room version. |
|
/// - Upgrade to a version lower than the server's default, if the server's |
|
/// default is stable. |
|
/// - Upgrade to an experimental version, unless it is the current version |
|
/// or the server's default. |
|
/// |
|
/// If the server's default is experimental, we also allow to upgrade to the |
|
/// highest stable version. |
|
pub(crate) fn with_room_versions( |
|
mut self, |
|
current_room_version: &RoomVersionId, |
|
capability: &RoomVersionsCapability, |
|
) -> Self { |
|
let current_is_stable = capability.is_stable_version(current_room_version); |
|
let default_is_stable = capability.is_stable_version(&capability.default); |
|
let maximum_stable_version = capability.maximum_stable_version(); |
|
|
|
// The minimum stable version is the highest stable version between the current |
|
// version and the default version. |
|
let minimum_stable_version = match (current_is_stable, default_is_stable) { |
|
(true, false) => Some(current_room_version), |
|
(false, true) => Some(&capability.default), |
|
(true, true) => Some( |
|
match numeric_sort::cmp(current_room_version.as_ref(), capability.default.as_ref()) |
|
{ |
|
Ordering::Less => &capability.default, |
|
Ordering::Equal | Ordering::Greater => current_room_version, |
|
}, |
|
), |
|
(false, false) => None, |
|
}; |
|
let selected_room_version = minimum_stable_version.unwrap_or(&capability.default); |
|
|
|
self.stable_room_versions = if let Some(minimum) = minimum_stable_version { |
|
// Keep all the stable versions higher than the minimum. |
|
capability |
|
.available |
|
.iter() |
|
.filter_map(|(version, stability)| { |
|
// Discard unstable versions. |
|
if *stability != RoomVersionStability::Stable { |
|
return None; |
|
} |
|
|
|
if numeric_sort::cmp(version.as_ref(), minimum.as_ref()) != Ordering::Less |
|
|| maximum_stable_version.is_some_and(|maximum| maximum == version) |
|
{ |
|
Some(version) |
|
} else { |
|
None |
|
} |
|
}) |
|
.cloned() |
|
.collect() |
|
} else { |
|
// The only allowed stable version will be the maximum. |
|
maximum_stable_version.into_iter().cloned().collect() |
|
}; |
|
|
|
// Add the current and default room versions if they are unstable. |
|
let current_is_default = *current_room_version == capability.default; |
|
self.unstable_room_versions = Some(current_room_version) |
|
.filter(|_| !current_is_stable) |
|
.cloned() |
|
.into_iter() |
|
.chain( |
|
Some(&capability.default) |
|
.filter(|_| !current_is_default && !default_is_stable) |
|
.cloned(), |
|
) |
|
.collect::<Vec<_>>(); |
|
|
|
// Sort all the versions. |
|
numeric_sort::sort_unstable(&mut self.stable_room_versions); |
|
numeric_sort::sort_unstable(&mut self.unstable_room_versions); |
|
|
|
// Find the position of the selected version. |
|
self.selected = self |
|
.stable_room_versions |
|
.binary_search_by(|version| { |
|
numeric_sort::cmp(version.as_ref(), selected_room_version.as_ref()) |
|
}) |
|
.or_else(|_| { |
|
self.unstable_room_versions |
|
.binary_search_by(|version| { |
|
numeric_sort::cmp(version.as_ref(), selected_room_version.as_ref()) |
|
}) |
|
.map(|pos| self.stable_room_versions.len() + pos) |
|
}) |
|
.unwrap_or_default(); |
|
|
|
self |
|
} |
|
|
|
/// Add information about the privileged creators changes. |
|
pub(crate) fn with_privileged_creators( |
|
mut self, |
|
own_creator: &OwnedUserId, |
|
privileged_creators: &[OwnedUserId], |
|
) -> Self { |
|
self.own_user_is_creator = privileged_creators.contains(own_creator); |
|
self.other_creators_count = |
|
privileged_creators.len() - usize::from(self.own_user_is_creator); |
|
self |
|
} |
|
} |
|
|
|
/// Helper trait for [`RoomVersionsCapability`]. |
|
trait RoomVersionsCapabilityExt { |
|
/// Whether the given room version is stable. |
|
fn is_stable_version(&self, version: &RoomVersionId) -> bool; |
|
|
|
/// The maximum stable room version in these capabilities. |
|
fn maximum_stable_version(&self) -> Option<&RoomVersionId>; |
|
} |
|
|
|
impl RoomVersionsCapabilityExt for RoomVersionsCapability { |
|
fn is_stable_version(&self, version: &RoomVersionId) -> bool { |
|
self.available |
|
.get(version) |
|
.is_some_and(|stability| *stability == RoomVersionStability::Stable) |
|
} |
|
|
|
fn maximum_stable_version(&self) -> Option<&RoomVersionId> { |
|
self.available |
|
.iter() |
|
.fold(None, |maximum, (version, stability)| { |
|
// Discard unstable versions. |
|
if *stability != RoomVersionStability::Stable { |
|
return maximum; |
|
} |
|
|
|
// Keep the maximum. |
|
if maximum.is_none_or(|maximum| { |
|
numeric_sort::cmp(version.as_ref(), maximum.as_ref()) == Ordering::Greater |
|
}) { |
|
Some(version) |
|
} else { |
|
maximum |
|
} |
|
}) |
|
} |
|
}
|
|
|