diff --git a/src/components/avatar/editable.rs b/src/components/avatar/editable.rs index e028c24b..c3a95fa5 100644 --- a/src/components/avatar/editable.rs +++ b/src/components/avatar/editable.rs @@ -20,7 +20,7 @@ use crate::{ image::{ImageError, IMAGE_QUEUE}, FrameDimensions, }, - BoundObject, BoundObjectWeakRef, CountedRef, + BoundObject, BoundObjectWeakRef, CountedRef, SingleItemListModel, }, }; @@ -505,12 +505,11 @@ impl EditableAvatar { /// Choose a new avatar. pub(super) async fn choose_avatar(&self) { - let filters = gio::ListStore::new::(); - let image_filter = gtk::FileFilter::new(); image_filter.set_name(Some(&gettext("Images"))); image_filter.add_mime_type("image/*"); - filters.append(&image_filter); + + let filters = SingleItemListModel::new(&image_filter); let dialog = gtk::FileDialog::builder() .title(gettext("Choose Avatar")) diff --git a/src/session/model/room/timeline/event/mod.rs b/src/session/model/room/timeline/event/mod.rs index bd3ec5e5..27748bde 100644 --- a/src/session/model/room/timeline/event/mod.rs +++ b/src/session/model/room/timeline/event/mod.rs @@ -61,7 +61,7 @@ pub struct UserReadReceipt { mod imp { use std::{ - cell::{Cell, RefCell}, + cell::{Cell, OnceCell, RefCell}, marker::PhantomData, sync::LazyLock, }; @@ -70,7 +70,7 @@ mod imp { use super::*; - #[derive(Debug, glib::Properties)] + #[derive(Debug, Default, glib::Properties)] #[properties(wrapper_type = super::Event)] pub struct Event { /// The underlying SDK timeline item. @@ -128,37 +128,13 @@ mod imp { #[property(get)] reactions: ReactionList, /// The read receipts on this event. - #[property(get)] - read_receipts: gio::ListStore, + #[property(get = Self::read_receipts_owned)] + read_receipts: OnceCell, /// Whether this event has any read receipt. #[property(get = Self::has_read_receipts)] has_read_receipts: PhantomData, } - impl Default for Event { - fn default() -> Self { - Self { - item: Default::default(), - event_id_string: Default::default(), - sender_id_string: Default::default(), - timestamp: Default::default(), - formatted_timestamp: Default::default(), - source: Default::default(), - has_source: Default::default(), - state: Default::default(), - is_edited: Default::default(), - latest_edit_source: Default::default(), - latest_edit_event_id_string: Default::default(), - latest_edit_timestamp: Default::default(), - latest_edit_formatted_timestamp: Default::default(), - is_highlighted: Default::default(), - reactions: Default::default(), - read_receipts: gio::ListStore::new::(), - has_read_receipts: Default::default(), - } - } - } - #[glib::object_subclass] impl ObjectSubclass for Event { const NAME: &'static str = "RoomEvent"; @@ -430,16 +406,27 @@ mod imp { self.item().is_highlighted() } + /// The read receipts on this event. + fn read_receipts(&self) -> &gio::ListStore { + self.read_receipts + .get_or_init(gio::ListStore::new::) + } + + /// The owned read receipts on this event. + fn read_receipts_owned(&self) -> gio::ListStore { + self.read_receipts().clone() + } + /// Update the read receipts list with the given receipts. fn update_read_receipts(&self, new_read_receipts: &IndexMap) { - let old_count = self.read_receipts.n_items(); + let old_count = self.read_receipts().n_items(); let new_count = new_read_receipts.len() as u32; if old_count == new_count { let mut is_all_same = true; for (i, new_user_id) in new_read_receipts.keys().enumerate() { let Some(old_receipt) = self - .read_receipts + .read_receipts() .item(i as u32) .and_downcast::() else { @@ -467,7 +454,8 @@ mod imp { }) }) .collect::>(); - self.read_receipts.splice(0, old_count, &new_read_receipts); + self.read_receipts() + .splice(0, old_count, &new_read_receipts); let prev_has_read_receipts = old_count > 0; let has_read_receipts = new_count > 0; @@ -479,7 +467,7 @@ mod imp { /// Whether this event has any read receipt. fn has_read_receipts(&self) -> bool { - self.read_receipts.n_items() > 0 + self.read_receipts().n_items() > 0 } } } diff --git a/src/session/model/room/timeline/mod.rs b/src/session/model/room/timeline/mod.rs index 645a4d7a..40e41c76 100644 --- a/src/session/model/room/timeline/mod.rs +++ b/src/session/model/room/timeline/mod.rs @@ -36,7 +36,11 @@ pub(crate) use self::{ virtual_item::{VirtualItem, VirtualItemKind}, }; use super::Room; -use crate::{prelude::*, spawn, spawn_tokio, utils::LoadingState}; +use crate::{ + prelude::*, + spawn, spawn_tokio, + utils::{LoadingState, SingleItemListModel}, +}; /// The number of events to request when loading more history. const MAX_BATCH_SIZE: u16 = 20; @@ -53,7 +57,7 @@ mod imp { use super::*; - #[derive(Debug, glib::Properties)] + #[derive(Debug, Default, glib::Properties)] #[properties(wrapper_type = super::Timeline)] pub struct Timeline { /// The room containing this timeline. @@ -64,14 +68,16 @@ mod imp { /// Items added at the start of the timeline. /// /// Currently this can only contain one item at a time. - start_items: gio::ListStore, + start_items: OnceCell, /// Items provided by the SDK timeline. - pub(super) sdk_items: gio::ListStore, + sdk_items: OnceCell, /// Items added at the end of the timeline. - end_items: gio::ListStore, + /// + /// Currently this can only contain one item at a time. + end_items: OnceCell, /// The `GListModel` containing all the timeline items. - #[property(get)] - items: gtk::FlattenListModel, + #[property(get = Self::items)] + items: OnceCell, /// A Hashmap linking a `TimelineEventItemId` to the corresponding /// `Event`. pub(super) event_map: RefCell>, @@ -95,37 +101,6 @@ mod imp { read_receipts_changed_handle: OnceCell, } - impl Default for Timeline { - fn default() -> Self { - let start_items = gio::ListStore::new::(); - let sdk_items = gio::ListStore::new::(); - let end_items = gio::ListStore::new::(); - - let model_list = gio::ListStore::new::(); - model_list.append(&start_items); - model_list.append(&sdk_items); - model_list.append(&end_items); - - Self { - room: Default::default(), - matrix_timeline: Default::default(), - start_items, - sdk_items, - end_items, - items: gtk::FlattenListModel::new(Some(model_list)), - event_map: Default::default(), - state: Default::default(), - is_loading_start: Default::default(), - is_empty: Default::default(), - has_reached_start: Default::default(), - preload: Default::default(), - diff_handle: Default::default(), - back_pagination_status_handle: Default::default(), - read_receipts_changed_handle: Default::default(), - } - } - } - #[glib::object_subclass] impl ObjectSubclass for Timeline { const NAME: &'static str = "Timeline"; @@ -240,7 +215,7 @@ mod imp { let diff_handle = spawn_tokio!(fut); self.diff_handle .set(diff_handle.abort_handle()) - .expect("handle is uninitialized"); + .expect("handle should be uninitialized"); self.watch_read_receipts().await; @@ -255,12 +230,49 @@ mod imp { pub(super) fn matrix_timeline(&self) -> &Arc { self.matrix_timeline .get() - .expect("matrix timeline is initialized") + .expect("matrix timeline should be initialized") + } + + /// Items added at the start of the timeline. + fn start_items(&self) -> &SingleItemListModel { + self.start_items.get_or_init(|| { + let model = SingleItemListModel::new(&VirtualItem::spinner(&self.obj())); + model.set_is_hidden(true); + model + }) + } + + /// Items provided by the SDK timeline. + pub(super) fn sdk_items(&self) -> &gio::ListStore { + self.sdk_items + .get_or_init(gio::ListStore::new::) + } + + /// Items added at the end of the timeline. + fn end_items(&self) -> &SingleItemListModel { + self.end_items.get_or_init(|| { + let model = SingleItemListModel::new(&VirtualItem::typing(&self.obj())); + model.set_is_hidden(true); + model + }) + } + + /// The `GListModel` containing all the timeline items. + fn items(&self) -> gtk::FlattenListModel { + self.items + .get_or_init(|| { + let model_list = gio::ListStore::new::(); + model_list.append(self.start_items()); + model_list.append(self.sdk_items()); + model_list.append(self.end_items()); + gtk::FlattenListModel::new(Some(model_list)) + }) + .clone() } /// Whether the timeline is empty. fn is_empty(&self) -> bool { - self.sdk_items.n_items() == 0 + self.sdk_items().n_items() == 0 } /// Set the loading state of the timeline. @@ -294,7 +306,7 @@ mod imp { self.is_loading_start.set(is_loading_start); self.update_loading_state(); - self.update_start_items(); + self.start_items().set_is_hidden(!is_loading_start); self.obj().notify_is_loading_start(); } @@ -310,26 +322,6 @@ mod imp { self.obj().notify_has_reached_start(); } - /// Update the virtual items at the start of the timeline. - fn update_start_items(&self) { - let n_items = self.start_items.n_items(); - - if self.is_loading_start.get() { - let has_item = self - .start_items - .item(0) - .and_downcast::() - .is_some_and(|item| item.is_spinner()); - - if !has_item { - self.start_items - .splice(0, 0, &[VirtualItem::spinner(&self.obj())]); - } - } else if n_items > 0 { - self.start_items.remove_all(); - } - } - /// Clear the state of the timeline. /// /// This doesn't handle removing items in `sdk_items` because it can be @@ -364,7 +356,7 @@ mod imp { /// Preload the timeline, if there are not enough items. async fn preload(&self) { - if self.sdk_items.n_items() < u32::from(MAX_BATCH_SIZE) { + if self.sdk_items().n_items() < u32::from(MAX_BATCH_SIZE) { self.paginate_backwards(|| ControlFlow::Break(())).await; } } @@ -425,10 +417,10 @@ mod imp { .map(|item| self.create_item(&item)) .collect::>(); - self.update_items(self.sdk_items.n_items(), 0, &new_list); + self.update_items(self.sdk_items().n_items(), 0, &new_list); } VectorDiff::Clear => { - self.sdk_items.remove_all(); + self.sdk_items().remove_all(); self.clear(); } VectorDiff::PushFront { value } => { @@ -437,13 +429,13 @@ mod imp { } VectorDiff::PushBack { value } => { let item = self.create_item(&value); - self.update_items(self.sdk_items.n_items(), 0, &[item]); + self.update_items(self.sdk_items().n_items(), 0, &[item]); } VectorDiff::PopFront => { self.update_items(0, 1, &[]); } VectorDiff::PopBack => { - self.update_items(self.sdk_items.n_items().saturating_sub(1), 1, &[]); + self.update_items(self.sdk_items().n_items().saturating_sub(1), 1, &[]); } VectorDiff::Insert { index, value } => { let item = self.create_item(&value); @@ -470,14 +462,14 @@ mod imp { } VectorDiff::Truncate { length } => { let length = length as u32; - let old_len = self.sdk_items.n_items(); + let old_len = self.sdk_items().n_items(); self.update_items(length, old_len.saturating_sub(length), &[]); } VectorDiff::Reset { values } => { // Reset the state. self.clear(); - let removed = self.sdk_items.n_items(); + let removed = self.sdk_items().n_items(); let new_list = values .into_iter() .map(|item| self.create_item(&item)) @@ -490,7 +482,7 @@ mod imp { /// Get the item at the given position. fn item_at(&self, pos: u32) -> Option { - self.sdk_items.item(pos).and_downcast() + self.sdk_items().item(pos).and_downcast() } /// Update the items at the given position by removing the given number @@ -506,7 +498,7 @@ mod imp { self.remove_item(&item); } - self.sdk_items.splice(pos, n_removals, additions); + self.sdk_items().splice(pos, n_removals, additions); // Update the header visibility of all the new additions, and the first item // after this batch. @@ -523,7 +515,7 @@ mod imp { /// Update the headers of the item at the given position and the given /// number of items after it. fn update_items_headers(&self, pos: u32, nb: u32) { - let sdk_items = &self.sdk_items; + let sdk_items = self.sdk_items(); let mut previous_sender = if pos > 0 { sdk_items @@ -639,27 +631,18 @@ mod imp { } } - /// Whether the timeline has a typing row. - fn has_typing_row(&self) -> bool { - self.end_items.n_items() > 0 - } - /// Add the typing row to the timeline, if it isn't present already. fn add_typing_row(&self) { - if self.has_typing_row() { - return; - } - - self.end_items.append(&VirtualItem::typing(&self.obj())); + self.end_items().set_is_hidden(false); } /// Remove the typing row from the timeline. pub(super) fn remove_empty_typing_row(&self) { - if !self.has_typing_row() || !self.room().typing_list().is_empty() { + if !self.room().typing_list().is_empty() { return; } - self.end_items.remove_all(); + self.end_items().set_is_hidden(true); } /// Listen to read receipts changes. @@ -704,7 +687,7 @@ mod imp { type Data = Arc; fn items(&self) -> Vec { - self.sdk_items + self.sdk_items() .snapshot() .into_iter() .map(|obj| { @@ -820,7 +803,7 @@ mod imp { /// Log the items in this timeline. fn log_items(&self) { let items = self - .sdk_items + .sdk_items() .iter::() .filter_map(|item| item.as_ref().map(item_to_log).ok()) .collect::>(); @@ -852,7 +835,7 @@ mod imp { fn item_to_log(item: &TimelineItem) -> String { if let Some(virtual_item) = item.downcast_ref::() { - format!("virtual::{:?}", virtual_item.kind().0) + format!("virtual::{:?}", virtual_item.kind()) } else if let Some(event) = item.downcast_ref::() { format!("event::{:?}", event.identifier()) } else { @@ -962,7 +945,7 @@ impl Timeline { .await .expect("task was not aborted"); - let sdk_items = &self.imp().sdk_items; + let sdk_items = self.imp().sdk_items(); let count = sdk_items.n_items(); for pos in (0..count).rev() { @@ -990,7 +973,7 @@ impl Timeline { pub(crate) fn redactable_events_for(&self, user_id: &UserId) -> Vec { let mut events = vec![]; - for item in self.imp().sdk_items.iter::() { + for item in self.imp().sdk_items().iter::() { let Ok(item) = item else { // The iterator is broken. break; diff --git a/src/session/model/room/timeline/virtual_item.rs b/src/session/model/room/timeline/virtual_item.rs index 21941e09..a7c9358c 100644 --- a/src/session/model/room/timeline/virtual_item.rs +++ b/src/session/model/room/timeline/virtual_item.rs @@ -1,15 +1,12 @@ -use std::ops::Deref; - -use gtk::{glib, prelude::*, subclass::prelude::*}; +use gtk::{glib, glib::closure_local, prelude::*, subclass::prelude::*}; use matrix_sdk_ui::timeline::VirtualTimelineItem; -use ruma::MilliSecondsSinceUnixEpoch; use super::{Timeline, TimelineItem, TimelineItemImpl}; use crate::utils::matrix::timestamp_to_date; /// The kind of virtual item. -#[derive(Debug, Default, Eq, PartialEq, Clone)] -pub enum VirtualItemKind { +#[derive(Debug, Default, Clone, Eq, PartialEq)] +pub(crate) enum VirtualItemKind { /// A spinner, when the timeline is loading. #[default] Spinner, @@ -26,41 +23,27 @@ pub enum VirtualItemKind { } impl VirtualItemKind { - /// Construct the `DayDivider` from the given timestamp. - fn with_timestamp(ts: MilliSecondsSinceUnixEpoch) -> Self { - Self::DayDivider(timestamp_to_date(ts)) - } - - /// Convert this into a [`BoxedVirtualItemKind`]. - fn boxed(self) -> BoxedVirtualItemKind { - BoxedVirtualItemKind(self) - } -} - -/// A boxed [`VirtualItemKind`]. -#[derive(Clone, Debug, Default, PartialEq, Eq, glib::Boxed)] -#[boxed_type(name = "BoxedVirtualItemKind")] -pub struct BoxedVirtualItemKind(pub(super) VirtualItemKind); - -impl Deref for BoxedVirtualItemKind { - type Target = VirtualItemKind; - - fn deref(&self) -> &Self::Target { - &self.0 + /// Construct a `VirtualItemKind` from the given item. + fn with_item(item: &VirtualTimelineItem) -> Self { + match item { + VirtualTimelineItem::DateDivider(ts) => Self::DayDivider(timestamp_to_date(*ts)), + VirtualTimelineItem::ReadMarker => Self::NewMessages, + VirtualTimelineItem::TimelineStart => Self::TimelineStart, + } } } mod imp { - use std::cell::RefCell; + use std::{cell::RefCell, sync::LazyLock}; + + use glib::subclass::Signal; use super::*; - #[derive(Debug, Default, glib::Properties)] - #[properties(wrapper_type = super::VirtualItem)] + #[derive(Debug, Default)] pub struct VirtualItem { /// The kind of virtual item. - #[property(get, set, construct)] - kind: RefCell, + kind: RefCell, } #[glib::object_subclass] @@ -70,10 +53,28 @@ mod imp { type ParentType = TimelineItem; } - #[glib::derived_properties] - impl ObjectImpl for VirtualItem {} + impl ObjectImpl for VirtualItem { + fn signals() -> &'static [Signal] { + static SIGNALS: LazyLock> = + LazyLock::new(|| vec![Signal::builder("kind-changed").build()]); + SIGNALS.as_ref() + } + } impl TimelineItemImpl for VirtualItem {} + + impl VirtualItem { + /// Set the kind of virtual item. + pub(super) fn set_kind(&self, kind: VirtualItemKind) { + self.kind.replace(kind); + self.obj().emit_by_name::<()>("kind-changed", &[]); + } + + /// The kind of virtual item. + pub(super) fn kind(&self) -> VirtualItemKind { + self.kind.borrow().clone() + } + } } glib::wrapper! { @@ -86,11 +87,12 @@ glib::wrapper! { impl VirtualItem { /// Create a new `VirtualItem`. fn new(timeline: &Timeline, kind: VirtualItemKind, timeline_id: &str) -> Self { - glib::Object::builder() + let obj = glib::Object::builder::() .property("timeline", timeline) - .property("kind", kind.boxed()) .property("timeline-id", timeline_id) - .build() + .build(); + obj.imp().set_kind(kind); + obj } /// Create a new `VirtualItem` from a virtual timeline item. @@ -99,24 +101,19 @@ impl VirtualItem { item: &VirtualTimelineItem, timeline_id: &str, ) -> Self { - let kind = match item { - VirtualTimelineItem::DateDivider(ts) => VirtualItemKind::with_timestamp(*ts), - VirtualTimelineItem::ReadMarker => VirtualItemKind::NewMessages, - VirtualTimelineItem::TimelineStart => VirtualItemKind::TimelineStart, - }; - + let kind = VirtualItemKind::with_item(item); Self::new(timeline, kind, timeline_id) } + /// The kind of virtual item. + pub(crate) fn kind(&self) -> VirtualItemKind { + self.imp().kind() + } + /// Update this `VirtualItem` with the given virtual timeline item. pub(crate) fn update_with_item(&self, item: &VirtualTimelineItem) { - let kind = match item { - VirtualTimelineItem::DateDivider(ts) => VirtualItemKind::with_timestamp(*ts), - VirtualTimelineItem::ReadMarker => VirtualItemKind::NewMessages, - VirtualTimelineItem::TimelineStart => VirtualItemKind::TimelineStart, - }; - - self.set_kind(kind.boxed()); + let kind = VirtualItemKind::with_item(item); + self.imp().set_kind(kind); } /// Create a spinner virtual item. @@ -128,13 +125,19 @@ impl VirtualItem { ) } - /// Whether this is a spinner virtual item. - pub(crate) fn is_spinner(&self) -> bool { - self.kind().0 == VirtualItemKind::Spinner - } - /// Create a typing virtual item. pub(crate) fn typing(timeline: &Timeline) -> Self { Self::new(timeline, VirtualItemKind::Typing, "VirtualItemKind::Typing") } + + /// Connect to the signal emitted when the kind changed. + pub fn connect_kind_changed(&self, f: F) -> glib::SignalHandlerId { + self.connect_closure( + "kind-changed", + true, + closure_local!(move |obj: Self| { + f(&obj); + }), + ) + } } diff --git a/src/session/view/account_settings/notifications_page.rs b/src/session/view/account_settings/notifications_page.rs index 2be95480..f6b9defd 100644 --- a/src/session/view/account_settings/notifications_page.rs +++ b/src/session/view/account_settings/notifications_page.rs @@ -8,7 +8,7 @@ use crate::{ i18n::gettext_f, session::model::{NotificationsGlobalSetting, NotificationsSettings}, spawn, toast, - utils::{BoundObjectWeakRef, DummyObject}, + utils::{BoundObjectWeakRef, DummyObject, SingleItemListModel}, }; mod imp { @@ -123,8 +123,7 @@ mod imp { ], ); - let extra_items = gio::ListStore::new::(); - extra_items.append(&DummyObject::new("add")); + let extra_items = SingleItemListModel::new(&DummyObject::new("add")); let all_items = gio::ListStore::new::(); all_items.append(&settings.keywords_list()); diff --git a/src/session/view/content/explore/mod.rs b/src/session/view/content/explore/mod.rs index c256f670..7780c911 100644 --- a/src/session/view/content/explore/mod.rs +++ b/src/session/view/content/explore/mod.rs @@ -18,7 +18,7 @@ use self::{server::ExploreServer, server_list::ExploreServerList, server_row::Ex use crate::{ components::LoadingRow, session::model::Session, - utils::{BoundObject, LoadingState}, + utils::{BoundObject, LoadingState, SingleItemListModel}, }; mod imp { @@ -56,7 +56,7 @@ mod imp { /// The list of public rooms. public_room_list: BoundObject, /// The items added at the end of the list. - end_items: OnceCell, + end_items: OnceCell, /// The full list model. full_model: OnceCell, } @@ -201,9 +201,12 @@ mod imp { } /// The items added at the end of the list. - fn end_items(&self) -> &gio::ListStore { - self.end_items - .get_or_init(gio::ListStore::new::) + fn end_items(&self) -> &SingleItemListModel { + self.end_items.get_or_init(|| { + let model = SingleItemListModel::new(&LoadingRow::new()); + model.set_is_hidden(true); + model + }) } /// The full list model. @@ -278,16 +281,8 @@ mod imp { let is_empty = public_room_list.is_empty(); // Create or remove the loading row, as needed. - let end_items = self.end_items(); - if matches!(loading_state, LoadingState::Loading) && !is_empty { - if end_items.n_items() == 0 { - // We need a loading row. - end_items.append(&LoadingRow::new()); - } - } else if end_items.n_items() > 0 { - // We do not need a loading row. - end_items.remove(0); - } + let show_loading_row = matches!(loading_state, LoadingState::Loading) && !is_empty; + self.end_items().set_is_hidden(!show_loading_row); // Update the visible page. let page_name = match loading_state { diff --git a/src/session/view/content/room_details/history_viewer/timeline.rs b/src/session/view/content/room_details/history_viewer/timeline.rs index 86dda85a..bded3cf0 100644 --- a/src/session/view/content/room_details/history_viewer/timeline.rs +++ b/src/session/view/content/room_details/history_viewer/timeline.rs @@ -13,7 +13,12 @@ use matrix_sdk::{ use tracing::error; use super::HistoryViewerEvent; -use crate::{components::LoadingRow, session::model::Room, spawn_tokio, utils::LoadingState}; +use crate::{ + components::LoadingRow, + session::model::Room, + spawn_tokio, + utils::{LoadingState, SingleItemListModel}, +}; mod imp { use std::cell::{Cell, OnceCell, RefCell}; @@ -41,8 +46,7 @@ mod imp { /// [`HistoryViewerEvent`]s. model_with_loading_item: OnceCell, /// A model containing a [`LoadingRow`] when the timeline is loading. - loading_item_model: OnceCell, - loading_row: LoadingRow, + loading_item_model: OnceCell, } #[glib::object_subclass] @@ -85,14 +89,8 @@ mod imp { self.state.set(state); - let loading_item_model = self.loading_item_model(); - if state == LoadingState::Loading { - if loading_item_model.n_items() == 0 { - loading_item_model.append(&self.loading_row); - } - } else if loading_item_model.n_items() != 0 { - loading_item_model.remove_all(); - } + self.loading_item_model() + .set_is_hidden(state != LoadingState::Loading); self.obj().notify_state(); } @@ -122,9 +120,12 @@ mod imp { } /// A model containing a [`LoadingRow`] when the timeline is loading. - pub(super) fn loading_item_model(&self) -> &gio::ListStore { - self.loading_item_model - .get_or_init(gio::ListStore::new::) + pub(super) fn loading_item_model(&self) -> &SingleItemListModel { + self.loading_item_model.get_or_init(|| { + let model = SingleItemListModel::new(&LoadingRow::new()); + model.set_is_hidden(true); + model + }) } /// A wrapper model with an extra loading item at the end when diff --git a/src/session/view/content/room_details/membership_lists.rs b/src/session/view/content/room_details/membership_lists.rs index fea1c791..d08751d8 100644 --- a/src/session/view/content/room_details/membership_lists.rs +++ b/src/session/view/content/room_details/membership_lists.rs @@ -18,7 +18,7 @@ mod imp { use super::*; - #[derive(Debug, glib::Properties)] + #[derive(Debug, Default, glib::Properties)] #[properties(wrapper_type = super::MembershipLists)] pub struct MembershipLists { /// The list of all members. @@ -28,8 +28,8 @@ mod imp { #[property(get)] joined: OnceCell, /// The list of extra items in the joined list. - #[property(get)] - extra_joined_items: gio::ListStore, + #[property(get = Self::extra_joined_items_owned)] + extra_joined_items: OnceCell, /// The full list to present for joined members. #[property(get)] joined_full: OnceCell, @@ -47,21 +47,6 @@ mod imp { banned_is_empty: Cell, } - impl Default for MembershipLists { - fn default() -> Self { - Self { - members: Default::default(), - joined: Default::default(), - extra_joined_items: gio::ListStore::new::(), - 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"; @@ -72,6 +57,17 @@ mod imp { impl ObjectImpl for MembershipLists {} impl MembershipLists { + /// The list of extra items in the joined list. + fn extra_joined_items(&self) -> &gio::ListStore { + self.extra_joined_items + .get_or_init(gio::ListStore::new::) + } + + /// The owned list of extra items in the joined list. + fn extra_joined_items_owned(&self) -> gio::ListStore { + self.extra_joined_items().clone() + } + /// Set the list of all members. fn set_members(&self, members: MemberList) { // Watch the loading state. @@ -115,7 +111,7 @@ mod imp { .get_or_init(|| build_filtered_list(sorted_members.clone(), Membership::Join)); let model_list = gio::ListStore::new::(); - model_list.append(&self.extra_joined_items); + model_list.append(self.extra_joined_items()); model_list.append(joined); self.joined_full .set(gtk::FlattenListModel::new(Some(model_list)).upcast()) @@ -149,7 +145,7 @@ mod imp { /// Whether the extra joined items list contain a loading row. fn has_loading_row(&self) -> bool { - self.extra_joined_items + self.extra_joined_items() .item(0) .is_some_and(|item| item.is::()) } @@ -158,14 +154,16 @@ mod imp { fn update_loading_state(&self, state: LoadingState) { if state == LoadingState::Ready { if self.has_loading_row() { - self.extra_joined_items.remove(0); + self.extra_joined_items().remove(0); } return; } - let loading_row = if let Some(loading_row) = - self.extra_joined_items.item(0).and_downcast::() + let loading_row = if let Some(loading_row) = self + .extra_joined_items() + .item(0) + .and_downcast::() { loading_row } else { @@ -178,7 +176,7 @@ mod imp { } )); - self.extra_joined_items.insert(0, &loading_row); + self.extra_joined_items().insert(0, &loading_row); loading_row }; @@ -190,7 +188,7 @@ mod imp { /// Whether the extra joined items list contain a membership subpage /// item for the given membership at the given position. fn has_membership_item_at(&self, membership: Membership, position: u32) -> bool { - self.extra_joined_items + self.extra_joined_items() .item(position) .and_downcast::() .is_some_and(|item| item.membership() == membership) @@ -216,13 +214,13 @@ mod imp { let has_invite_row = self.has_membership_item_at(Membership::Invite, position); if is_empty && has_invite_row { - self.extra_joined_items.remove(position); + 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.extra_joined_items().insert(position, &invite_item); } self.obj().notify_invited_is_empty(); @@ -250,13 +248,13 @@ mod imp { let has_ban_row = self.has_membership_item_at(Membership::Ban, position); if is_empty && has_ban_row { - self.extra_joined_items.remove(position); + 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.extra_joined_items().insert(position, &invite_item); } self.obj().notify_banned_is_empty(); diff --git a/src/session/view/content/room_history/item_row.rs b/src/session/view/content/room_history/item_row.rs index dde927b5..accffa6a 100644 --- a/src/session/view/content/room_history/item_row.rs +++ b/src/session/view/content/room_history/item_row.rs @@ -312,7 +312,7 @@ mod imp { self.obj().set_popover(None); self.update_event_actions(None); - let kind_handler = virtual_item.connect_kind_notify(clone!( + let kind_handler = virtual_item.connect_kind_changed(clone!( #[weak(rename_to = imp)] self, move |virtual_item| { @@ -327,7 +327,7 @@ mod imp { /// Construct the widget for the given virtual item. fn build_virtual_item(&self, virtual_item: &VirtualItem) { let obj = self.obj(); - let kind = &*virtual_item.kind(); + let kind = &virtual_item.kind(); match kind { VirtualItemKind::Spinner => { diff --git a/src/utils/single_item_list_model.rs b/src/utils/single_item_list_model.rs index 1e163a72..8f40d1d4 100644 --- a/src/utils/single_item_list_model.rs +++ b/src/utils/single_item_list_model.rs @@ -1,7 +1,7 @@ use gtk::{gio, glib, prelude::*, subclass::prelude::*}; mod imp { - use std::cell::OnceCell; + use std::cell::{Cell, OnceCell}; use super::*; @@ -10,7 +10,10 @@ mod imp { pub struct SingleItemListModel { /// The item contained by this model. #[property(get, construct_only)] - inner_item: OnceCell, + item: OnceCell, + /// Whether the item is hidden. + #[property(get, set = Self::set_is_hidden, explicit_notify)] + is_hidden: Cell, } #[glib::object_subclass] @@ -25,22 +28,38 @@ mod imp { impl ListModelImpl for SingleItemListModel { fn item_type(&self) -> glib::Type { - self.inner_item().type_() + self.item().type_() } fn n_items(&self) -> u32 { - 1 + 1 - u32::from(self.is_hidden.get()) } fn item(&self, position: u32) -> Option { - (position == 0).then(|| self.inner_item().clone().upcast()) + (!self.is_hidden.get() && position == 0).then(|| self.item().clone().upcast()) } } impl SingleItemListModel { /// The item contained by this model. - fn inner_item(&self) -> &glib::Object { - self.inner_item.get().expect("inner item was initialized") + fn item(&self) -> &glib::Object { + self.item.get().expect("item should be initialized") + } + + /// Set whether the item is hidden. + fn set_is_hidden(&self, hidden: bool) { + if self.is_hidden.get() == hidden { + return; + } + + self.is_hidden.set(hidden); + + let obj = self.obj(); + obj.notify_is_hidden(); + + let removed = (hidden).into(); + let added = (!hidden).into(); + obj.items_changed(0, removed, added); } } } @@ -54,6 +73,6 @@ glib::wrapper! { impl SingleItemListModel { /// Construct a new `SingleItemListModel` for the given item. pub fn new(item: &impl IsA) -> Self { - glib::Object::builder().property("inner-item", item).build() + glib::Object::builder().property("item", item).build() } }