fractal/session/view/content/room_details/
membership_lists.rsuse gettextrs::gettext;
use gtk::{
gio, glib,
glib::{clone, closure},
prelude::*,
subclass::prelude::*,
};
use super::MembershipSubpageItem;
use crate::{
components::LoadingRow,
session::model::{Member, MemberList, Membership},
utils::{BoundConstructOnlyObject, ExpressionListModel, LoadingState},
};
mod imp {
use std::cell::{Cell, OnceCell};
use super::*;
#[derive(Debug, glib::Properties)]
#[properties(wrapper_type = super::MembershipLists)]
pub struct MembershipLists {
#[property(get, set = Self::set_members)]
members: BoundConstructOnlyObject<MemberList>,
#[property(get)]
joined: OnceCell<gio::ListModel>,
#[property(get)]
extra_joined_items: gio::ListStore,
#[property(get)]
joined_full: OnceCell<gio::ListModel>,
#[property(get)]
invited: OnceCell<gio::ListModel>,
#[property(get)]
invited_is_empty: Cell<bool>,
#[property(get)]
banned: OnceCell<gio::ListModel>,
#[property(get)]
banned_is_empty: Cell<bool>,
}
impl Default for MembershipLists {
fn default() -> Self {
Self {
members: Default::default(),
joined: Default::default(),
extra_joined_items: gio::ListStore::new::<glib::Object>(),
joined_full: Default::default(),
invited: Default::default(),
invited_is_empty: Cell::new(true),
banned: Default::default(),
banned_is_empty: Cell::new(true),
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for MembershipLists {
const NAME: &'static str = "ContentMembershipLists";
type Type = super::MembershipLists;
}
#[glib::derived_properties]
impl ObjectImpl for MembershipLists {}
impl MembershipLists {
fn set_members(&self, members: MemberList) {
let signal_handler_ids = vec![members.connect_state_notify(clone!(
#[weak(rename_to = imp)]
self,
move |members| {
imp.update_loading_state(members.state());
}
))];
self.members.set(members.clone(), signal_handler_ids);
self.update_loading_state(members.state());
let power_level_expr = Member::this_expression("power-level");
let sorter = gtk::MultiSorter::new();
sorter.append(
gtk::NumericSorter::builder()
.expression(&power_level_expr)
.sort_order(gtk::SortType::Descending)
.build(),
);
let display_name_expr = Member::this_expression("display-name");
sorter.append(gtk::StringSorter::new(Some(&display_name_expr)));
let expr_members = ExpressionListModel::new();
expr_members.set_expressions(vec![
power_level_expr.upcast(),
display_name_expr.upcast(),
Member::this_expression("membership").upcast(),
]);
expr_members.set_model(Some(members));
let sorted_members = gtk::SortListModel::new(Some(expr_members), Some(sorter));
let joined = self
.joined
.get_or_init(|| build_filtered_list(sorted_members.clone(), Membership::Join));
let model_list = gio::ListStore::new::<gio::ListModel>();
model_list.append(&self.extra_joined_items);
model_list.append(joined);
self.joined_full
.set(gtk::FlattenListModel::new(Some(model_list)).upcast())
.expect("full list for joined members is uninitialized");
let invited = self
.invited
.get_or_init(|| build_filtered_list(sorted_members.clone(), Membership::Invite));
invited.connect_items_changed(clone!(
#[weak(rename_to = imp)]
self,
move |_, _, _, _| {
imp.update_invited();
}
));
let banned = self
.banned
.get_or_init(|| build_filtered_list(sorted_members, Membership::Ban));
banned.connect_items_changed(clone!(
#[weak(rename_to = imp)]
self,
move |_, _, _, _| {
imp.update_banned();
}
));
self.update_invited();
self.update_banned();
}
fn has_loading_row(&self) -> bool {
self.extra_joined_items
.item(0)
.is_some_and(|item| item.is::<LoadingRow>())
}
fn update_loading_state(&self, state: LoadingState) {
if state == LoadingState::Ready {
if self.has_loading_row() {
self.extra_joined_items.remove(0);
}
return;
}
let loading_row = if let Some(loading_row) =
self.extra_joined_items.item(0).and_downcast::<LoadingRow>()
{
loading_row
} else {
let loading_row = LoadingRow::new();
loading_row.connect_retry(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.members.obj().reload();
}
));
self.extra_joined_items.insert(0, &loading_row);
loading_row
};
let error = (state == LoadingState::Error)
.then(|| gettext("Could not load the full list of room members"));
loading_row.set_error(error.as_deref());
}
fn has_membership_item_at(&self, membership: Membership, position: u32) -> bool {
self.extra_joined_items
.item(position)
.and_downcast::<MembershipSubpageItem>()
.is_some_and(|item| item.membership() == membership)
}
fn update_invited(&self) {
let was_empty = self.invited_is_empty.get();
let is_empty = self
.invited
.get()
.expect("invited members are initialized")
.n_items()
== 0;
if was_empty == is_empty {
return;
}
self.invited_is_empty.set(is_empty);
let position = self.has_loading_row().into();
let has_invite_row = self.has_membership_item_at(Membership::Invite, position);
if is_empty && has_invite_row {
self.extra_joined_items.remove(position);
} else if !is_empty && !has_invite_row {
let invite_item = MembershipSubpageItem::new(
Membership::Invite,
self.invited.get().expect("invited members are initialized"),
);
self.extra_joined_items.insert(position, &invite_item);
}
self.obj().notify_invited_is_empty();
}
fn update_banned(&self) {
let was_empty = self.banned_is_empty.get();
let is_empty = self
.banned
.get()
.expect("banned members are initialized")
.n_items()
== 0;
if was_empty == is_empty {
return;
}
self.banned_is_empty.set(is_empty);
let mut position = u32::from(self.has_loading_row());
position += u32::from(self.has_membership_item_at(Membership::Invite, position));
let has_ban_row = self.has_membership_item_at(Membership::Ban, position);
if is_empty && has_ban_row {
self.extra_joined_items.remove(position);
} else if !is_empty && !has_ban_row {
let invite_item = MembershipSubpageItem::new(
Membership::Ban,
self.banned.get().expect("banned members are initialized"),
);
self.extra_joined_items.insert(position, &invite_item);
}
self.obj().notify_banned_is_empty();
}
}
}
glib::wrapper! {
pub struct MembershipLists(ObjectSubclass<imp::MembershipLists>);
}
impl MembershipLists {
pub fn new() -> Self {
glib::Object::new()
}
}
impl Default for MembershipLists {
fn default() -> Self {
Self::new()
}
}
fn build_filtered_list(model: impl IsA<gio::ListModel>, state: Membership) -> gio::ListModel {
let membership_expression = Member::this_expression("membership").chain_closure::<bool>(
closure!(|_: Option<glib::Object>, this_state: Membership| this_state == state),
);
let membership_filter = gtk::BoolFilter::new(Some(&membership_expression));
let filter_model = gtk::FilterListModel::new(Some(model), Some(membership_filter));
filter_model.upcast()
}