|
|
|
|
@ -1,5 +1,5 @@
|
|
|
|
|
use adw::{prelude::*, subclass::prelude::*}; |
|
|
|
|
use gettextrs::ngettext; |
|
|
|
|
use gettextrs::{gettext, ngettext}; |
|
|
|
|
use gtk::{ |
|
|
|
|
gio, glib, |
|
|
|
|
glib::{clone, closure}, |
|
|
|
|
@ -10,20 +10,18 @@ mod item_row;
|
|
|
|
|
mod membership_subpage_row; |
|
|
|
|
|
|
|
|
|
use self::{item_row::ItemRow, membership_subpage_row::MembershipSubpageRow}; |
|
|
|
|
use super::membership_as_tag; |
|
|
|
|
use crate::{ |
|
|
|
|
prelude::*, |
|
|
|
|
session::{ |
|
|
|
|
model::{Member, Membership, Room}, |
|
|
|
|
view::content::room_details::MembershipSubpageItem, |
|
|
|
|
view::content::room_details::{MembershipLists, MembershipSubpageItem}, |
|
|
|
|
}, |
|
|
|
|
utils::expression, |
|
|
|
|
utils::{expression, LoadingState}, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
mod imp { |
|
|
|
|
use std::{ |
|
|
|
|
cell::{Cell, RefCell}, |
|
|
|
|
marker::PhantomData, |
|
|
|
|
}; |
|
|
|
|
use std::cell::{Cell, RefCell}; |
|
|
|
|
|
|
|
|
|
use glib::subclass::InitializingObject; |
|
|
|
|
|
|
|
|
|
@ -35,27 +33,41 @@ mod imp {
|
|
|
|
|
)] |
|
|
|
|
#[properties(wrapper_type = super::MembersListView)] |
|
|
|
|
pub struct MembersListView { |
|
|
|
|
#[template_child] |
|
|
|
|
search_button: TemplateChild<gtk::ToggleButton>, |
|
|
|
|
#[template_child] |
|
|
|
|
search_bar: TemplateChild<gtk::SearchBar>, |
|
|
|
|
#[template_child] |
|
|
|
|
search_entry: TemplateChild<gtk::SearchEntry>, |
|
|
|
|
#[template_child] |
|
|
|
|
stack: TemplateChild<gtk::Stack>, |
|
|
|
|
#[template_child] |
|
|
|
|
empty_stack_page: TemplateChild<gtk::StackPage>, |
|
|
|
|
#[template_child] |
|
|
|
|
empty_page: TemplateChild<adw::StatusPage>, |
|
|
|
|
#[template_child] |
|
|
|
|
empty_listbox: TemplateChild<gtk::ListBox>, |
|
|
|
|
#[template_child] |
|
|
|
|
members_stack_page: TemplateChild<gtk::StackPage>, |
|
|
|
|
#[template_child] |
|
|
|
|
list_view: TemplateChild<gtk::ListView>, |
|
|
|
|
/// The room containing the members to present.
|
|
|
|
|
#[property(get, set = Self::set_room, construct_only)] |
|
|
|
|
room: glib::WeakRef<Room>, |
|
|
|
|
/// The model used for this view.
|
|
|
|
|
#[property(get = Self::model, set = Self::set_model, construct_only)] |
|
|
|
|
model: PhantomData<Option<gio::ListModel>>, |
|
|
|
|
/// The lists of members filtered by membership for the room.
|
|
|
|
|
#[property(get, set = Self::set_membership_lists, construct_only)] |
|
|
|
|
membership_lists: glib::WeakRef<MembershipLists>, |
|
|
|
|
/// The model with the search filter.
|
|
|
|
|
filtered_model: gtk::FilterListModel, |
|
|
|
|
/// The membership used to filter the model.
|
|
|
|
|
#[property(get, construct_only, builder(Membership::default()))] |
|
|
|
|
#[property(get, set = Self::set_membership, construct_only, builder(Membership::default()))] |
|
|
|
|
membership: Cell<Membership>, |
|
|
|
|
/// Whether our own user can send an invite in the current room.
|
|
|
|
|
#[property(get, set = Self::set_can_invite, explicit_notify)] |
|
|
|
|
can_invite: Cell<bool>, |
|
|
|
|
members_state_handler: RefCell<Option<glib::SignalHandlerId>>, |
|
|
|
|
items_changed_handler: RefCell<Option<glib::SignalHandlerId>>, |
|
|
|
|
extra_items_changed_handler: RefCell<Option<glib::SignalHandlerId>>, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[glib::object_subclass] |
|
|
|
|
@ -68,6 +80,7 @@ mod imp {
|
|
|
|
|
ItemRow::ensure_type(); |
|
|
|
|
|
|
|
|
|
Self::bind_template(klass); |
|
|
|
|
Self::bind_template_callbacks(klass); |
|
|
|
|
|
|
|
|
|
klass.set_css_name("members-list"); |
|
|
|
|
} |
|
|
|
|
@ -81,7 +94,6 @@ mod imp {
|
|
|
|
|
impl ObjectImpl for MembersListView { |
|
|
|
|
fn constructed(&self) { |
|
|
|
|
self.parent_constructed(); |
|
|
|
|
let obj = self.obj(); |
|
|
|
|
|
|
|
|
|
// Needed because the GtkSearchEntry is not the direct child of the
|
|
|
|
|
// GtkSearchBear.
|
|
|
|
|
@ -116,41 +128,26 @@ mod imp {
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
self.filtered_model.set_filter(Some(&search_filter)); |
|
|
|
|
|
|
|
|
|
self.list_view.set_model(Some(>k::NoSelection::new(Some( |
|
|
|
|
self.filtered_model.clone(), |
|
|
|
|
)))); |
|
|
|
|
self.list_view.connect_activate(clone!( |
|
|
|
|
#[weak] |
|
|
|
|
obj, |
|
|
|
|
move |_, pos| { |
|
|
|
|
let Some(item) = obj.imp().filtered_model.item(pos) else { |
|
|
|
|
return; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
if let Some(member) = item.downcast_ref::<Member>() { |
|
|
|
|
obj.activate_action( |
|
|
|
|
"details.show-member", |
|
|
|
|
Some(&member.user_id().as_str().to_variant()), |
|
|
|
|
) |
|
|
|
|
.unwrap(); |
|
|
|
|
} else if let Some(item) = item.downcast_ref::<MembershipSubpageItem>() { |
|
|
|
|
obj.activate_action( |
|
|
|
|
"members.show-membership-list", |
|
|
|
|
Some(&item.membership().to_variant()), |
|
|
|
|
) |
|
|
|
|
.unwrap(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
)); |
|
|
|
|
|
|
|
|
|
self.update_title(); |
|
|
|
|
self.init_members_list(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fn dispose(&self) { |
|
|
|
|
if let Some(model) = self.model() { |
|
|
|
|
if let Some(membership_lists) = self.membership_lists.upgrade() { |
|
|
|
|
if let Some(handler) = self.members_state_handler.take() { |
|
|
|
|
membership_lists.members().disconnect(handler); |
|
|
|
|
} |
|
|
|
|
if let Some(handler) = self.items_changed_handler.take() { |
|
|
|
|
model.disconnect(handler); |
|
|
|
|
self.members_only_model(&membership_lists) |
|
|
|
|
.disconnect(handler); |
|
|
|
|
} |
|
|
|
|
if let Some(handler) = self.extra_items_changed_handler.take() { |
|
|
|
|
if let Some(model) = self.extra_items_model(&membership_lists) { |
|
|
|
|
model.disconnect(handler); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
@ -159,6 +156,7 @@ mod imp {
|
|
|
|
|
impl WidgetImpl for MembersListView {} |
|
|
|
|
impl NavigationPageImpl for MembersListView {} |
|
|
|
|
|
|
|
|
|
#[gtk::template_callbacks] |
|
|
|
|
impl MembersListView { |
|
|
|
|
/// Set the room containing the members to present.
|
|
|
|
|
fn set_room(&self, room: &Room) { |
|
|
|
|
@ -174,26 +172,26 @@ mod imp {
|
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// The model used for this view.
|
|
|
|
|
fn model(&self) -> Option<gio::ListModel> { |
|
|
|
|
self.filtered_model.model() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Set the model used for this view.
|
|
|
|
|
fn set_model(&self, model: &gio::ListModel) { |
|
|
|
|
self.filtered_model.set_model(Some(model)); |
|
|
|
|
/// Set the room containing the members to present.
|
|
|
|
|
fn set_membership_lists(&self, membership_lists: &MembershipLists) { |
|
|
|
|
self.membership_lists.set(Some(membership_lists)); |
|
|
|
|
|
|
|
|
|
let items_changed_handler = model.connect_items_changed(clone!( |
|
|
|
|
let members_state_handler = membership_lists.members().connect_state_notify(clone!( |
|
|
|
|
#[weak(rename_to = imp)] |
|
|
|
|
self, |
|
|
|
|
move |_, _, _, _| { |
|
|
|
|
imp.update_title(); |
|
|
|
|
move |_| { |
|
|
|
|
imp.update_view(); |
|
|
|
|
} |
|
|
|
|
)); |
|
|
|
|
self.items_changed_handler |
|
|
|
|
.replace(Some(items_changed_handler)); |
|
|
|
|
self.members_state_handler |
|
|
|
|
.replace(Some(members_state_handler)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
self.obj().notify_model(); |
|
|
|
|
/// Set the membership used to filter the model.
|
|
|
|
|
fn set_membership(&self, membership: Membership) { |
|
|
|
|
self.membership.set(membership); |
|
|
|
|
self.obj().set_tag(Some(membership_as_tag(membership))); |
|
|
|
|
self.update_empty_page(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Set whether our own user can send an invite in the current room.
|
|
|
|
|
@ -206,14 +204,84 @@ mod imp {
|
|
|
|
|
self.obj().notify_can_invite(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Update the page title for the current state.
|
|
|
|
|
fn update_title(&self) { |
|
|
|
|
let Some(model) = self.model() else { |
|
|
|
|
/// The full list model from the given lists of members used by the list
|
|
|
|
|
/// view.
|
|
|
|
|
fn full_model(&self, membership_lists: &MembershipLists) -> gio::ListModel { |
|
|
|
|
match self.membership.get() { |
|
|
|
|
Membership::Invite => membership_lists.invited(), |
|
|
|
|
Membership::Ban => membership_lists.banned(), |
|
|
|
|
_ => membership_lists.joined_full(), |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// The list model from the given lists of members containing only
|
|
|
|
|
/// members.
|
|
|
|
|
fn members_only_model(&self, membership_lists: &MembershipLists) -> gio::ListModel { |
|
|
|
|
match self.membership.get() { |
|
|
|
|
Membership::Invite => membership_lists.invited(), |
|
|
|
|
Membership::Ban => membership_lists.banned(), |
|
|
|
|
_ => membership_lists.joined(), |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// The list model from the given lists of members containing extra
|
|
|
|
|
/// items.
|
|
|
|
|
fn extra_items_model(&self, membership_lists: &MembershipLists) -> Option<gio::ListModel> { |
|
|
|
|
match self.membership.get() { |
|
|
|
|
Membership::Invite | Membership::Ban => None, |
|
|
|
|
_ => Some(membership_lists.extra_joined_items().upcast()), |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Initialize the members list used for this view.
|
|
|
|
|
fn init_members_list(&self) { |
|
|
|
|
let Some(membership_lists) = self.membership_lists.upgrade() else { |
|
|
|
|
return; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let full_model = self.full_model(&membership_lists); |
|
|
|
|
self.filtered_model.set_model(Some(&full_model)); |
|
|
|
|
|
|
|
|
|
let members_only_model = self.members_only_model(&membership_lists); |
|
|
|
|
let items_changed_handler = members_only_model.connect_items_changed(clone!( |
|
|
|
|
#[weak(rename_to = imp)] |
|
|
|
|
self, |
|
|
|
|
move |_, _, _, _| { |
|
|
|
|
imp.update_view(); |
|
|
|
|
} |
|
|
|
|
)); |
|
|
|
|
self.items_changed_handler |
|
|
|
|
.replace(Some(items_changed_handler)); |
|
|
|
|
|
|
|
|
|
if let Some(extra_items_model) = self.extra_items_model(&membership_lists) { |
|
|
|
|
let extra_items_changed_handler = extra_items_model.connect_items_changed(clone!( |
|
|
|
|
#[weak(rename_to = imp)] |
|
|
|
|
self, |
|
|
|
|
move |_, _, _, _| { |
|
|
|
|
imp.update_empty_listbox(); |
|
|
|
|
} |
|
|
|
|
)); |
|
|
|
|
self.extra_items_changed_handler |
|
|
|
|
.replace(Some(extra_items_changed_handler)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
self.update_view(); |
|
|
|
|
self.update_empty_listbox(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Update the view for the current state.
|
|
|
|
|
fn update_view(&self) { |
|
|
|
|
let Some(membership_lists) = self.membership_lists.upgrade() else { |
|
|
|
|
self.stack.set_visible_child_name("no-members"); |
|
|
|
|
return; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let model = self.members_only_model(&membership_lists); |
|
|
|
|
let count = model.n_items(); |
|
|
|
|
let title = match self.membership.get() { |
|
|
|
|
let is_empty = count == 0; |
|
|
|
|
let membership = self.membership.get(); |
|
|
|
|
|
|
|
|
|
let title = match membership { |
|
|
|
|
Membership::Invite => { |
|
|
|
|
ngettext("Invited Room Member", "Invited Room Members", count) |
|
|
|
|
} |
|
|
|
|
@ -222,6 +290,125 @@ mod imp {
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
self.obj().set_title(&title); |
|
|
|
|
self.members_stack_page.set_title(&title); |
|
|
|
|
|
|
|
|
|
let (visible_page, extra_items_model) = if is_empty { |
|
|
|
|
match membership_lists.members().state() { |
|
|
|
|
LoadingState::Initial | LoadingState::Loading => ("loading", None), |
|
|
|
|
LoadingState::Error => ("error", None), |
|
|
|
|
LoadingState::Ready => { |
|
|
|
|
let extra_items_model = self.extra_items_model(&membership_lists); |
|
|
|
|
("empty", extra_items_model) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
("members", None) |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
self.empty_listbox |
|
|
|
|
.bind_model(extra_items_model.as_ref(), |item| { |
|
|
|
|
let row = MembershipSubpageRow::new(); |
|
|
|
|
row.set_item(item.downcast_ref::<MembershipSubpageItem>().cloned()); |
|
|
|
|
|
|
|
|
|
row.upcast() |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Hide the search button and bar if the list is empty, since there is no search
|
|
|
|
|
// possible.
|
|
|
|
|
self.search_button.set_visible(!is_empty); |
|
|
|
|
self.search_bar.set_visible(!is_empty); |
|
|
|
|
|
|
|
|
|
self.stack.set_visible_child_name(visible_page); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Update the "empty" page for the current state.
|
|
|
|
|
fn update_empty_page(&self) { |
|
|
|
|
let membership = self.membership.get(); |
|
|
|
|
|
|
|
|
|
let (title, description) = match membership { |
|
|
|
|
Membership::Invite => { |
|
|
|
|
let title = gettext("No Invited Room Members"); |
|
|
|
|
let description = gettext("There are no invited members in this room"); |
|
|
|
|
(title, description) |
|
|
|
|
} |
|
|
|
|
Membership::Ban => { |
|
|
|
|
let title = gettext("No Banned Room Members"); |
|
|
|
|
let description = gettext("There are no banned members in this room"); |
|
|
|
|
(title, description) |
|
|
|
|
} |
|
|
|
|
_ => { |
|
|
|
|
let title = gettext("No Room Members"); |
|
|
|
|
let description = gettext("There are no members in this room"); |
|
|
|
|
(title, description) |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
self.empty_stack_page.set_title(&title); |
|
|
|
|
self.empty_page.set_title(&title); |
|
|
|
|
self.empty_page.set_description(Some(&description)); |
|
|
|
|
self.empty_page.set_icon_name(Some(membership.icon_name())); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Update the `GtkListBox` of the "empty" page for the current state.
|
|
|
|
|
fn update_empty_listbox(&self) { |
|
|
|
|
let has_extra_items = self |
|
|
|
|
.membership_lists |
|
|
|
|
.upgrade() |
|
|
|
|
.and_then(|membership_lists| self.extra_items_model(&membership_lists)) |
|
|
|
|
.is_some_and(|model| model.n_items() > 0); |
|
|
|
|
self.empty_listbox.set_visible(has_extra_items); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Activate the row of the members `GtkListView` at the given position.
|
|
|
|
|
#[template_callback] |
|
|
|
|
fn activate_listview_row(&self, pos: u32) { |
|
|
|
|
let Some(item) = self.filtered_model.item(pos) else { |
|
|
|
|
return; |
|
|
|
|
}; |
|
|
|
|
let obj = self.obj(); |
|
|
|
|
|
|
|
|
|
if let Some(member) = item.downcast_ref::<Member>() { |
|
|
|
|
obj.activate_action( |
|
|
|
|
"details.show-member", |
|
|
|
|
Some(&member.user_id().as_str().to_variant()), |
|
|
|
|
) |
|
|
|
|
.expect("action exists"); |
|
|
|
|
} else if let Some(item) = item.downcast_ref::<MembershipSubpageItem>() { |
|
|
|
|
obj.activate_action( |
|
|
|
|
"members.show-membership-list", |
|
|
|
|
Some(&item.membership().to_variant()), |
|
|
|
|
) |
|
|
|
|
.expect("action exists"); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Activate the given row from the `GtkListBox`.
|
|
|
|
|
#[template_callback] |
|
|
|
|
fn activate_listbox_row(&self, row: >k::ListBoxRow) { |
|
|
|
|
let row = row |
|
|
|
|
.downcast_ref::<MembershipSubpageRow>() |
|
|
|
|
.expect("list box contains only membership subpage rows"); |
|
|
|
|
|
|
|
|
|
let Some(item) = row.item() else { |
|
|
|
|
return; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
self.obj() |
|
|
|
|
.activate_action( |
|
|
|
|
"members.show-membership-list", |
|
|
|
|
Some(&item.membership().to_variant()), |
|
|
|
|
) |
|
|
|
|
.expect("action exists"); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Reload the list of members of the room.
|
|
|
|
|
#[template_callback] |
|
|
|
|
fn reload_members(&self) { |
|
|
|
|
let Some(membership_lists) = self.membership_lists.upgrade() else { |
|
|
|
|
return; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
membership_lists.members().reload(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
@ -233,19 +420,13 @@ glib::wrapper! {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl MembersListView { |
|
|
|
|
/// Construct a new `MembersListView` with the given room, list and
|
|
|
|
|
/// membership.
|
|
|
|
|
pub fn new( |
|
|
|
|
room: &Room, |
|
|
|
|
model: &impl IsA<gio::ListModel>, |
|
|
|
|
membership: Membership, |
|
|
|
|
tag: &str, |
|
|
|
|
) -> Self { |
|
|
|
|
/// Construct a new `MembersListView` with the given room, membership lists
|
|
|
|
|
/// and membership.
|
|
|
|
|
pub fn new(room: &Room, membership_lists: &MembershipLists, membership: Membership) -> Self { |
|
|
|
|
glib::Object::builder() |
|
|
|
|
.property("room", room) |
|
|
|
|
.property("model", model) |
|
|
|
|
.property("membership-lists", membership_lists) |
|
|
|
|
.property("membership", membership) |
|
|
|
|
.property("tag", tag) |
|
|
|
|
.build() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|