Browse Source

misc: Use SingleItemListModel where possible

Instead of GListStore.
merge-requests/1958/merge
Kévin Commaille 11 months ago
parent
commit
d870c1497d
No known key found for this signature in database
GPG Key ID: C971D9DBC9D678D
  1. 7
      src/components/avatar/editable.rs
  2. 52
      src/session/model/room/timeline/event/mod.rs
  3. 161
      src/session/model/room/timeline/mod.rs
  4. 113
      src/session/model/room/timeline/virtual_item.rs
  5. 5
      src/session/view/account_settings/notifications_page.rs
  6. 25
      src/session/view/content/explore/mod.rs
  7. 29
      src/session/view/content/room_details/history_viewer/timeline.rs
  8. 56
      src/session/view/content/room_details/membership_lists.rs
  9. 4
      src/session/view/content/room_history/item_row.rs
  10. 35
      src/utils/single_item_list_model.rs

7
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::<gtk::FileFilter>();
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"))

52
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<gio::ListStore>,
/// Whether this event has any read receipt.
#[property(get = Self::has_read_receipts)]
has_read_receipts: PhantomData<bool>,
}
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::<glib::BoxedAnyObject>(),
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::<glib::BoxedAnyObject>)
}
/// 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<OwnedUserId, Receipt>) {
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::<glib::BoxedAnyObject>()
else {
@ -467,7 +454,8 @@ mod imp {
})
})
.collect::<Vec<_>>();
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
}
}
}

161
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<SingleItemListModel>,
/// Items provided by the SDK timeline.
pub(super) sdk_items: gio::ListStore,
sdk_items: OnceCell<gio::ListStore>,
/// 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<SingleItemListModel>,
/// The `GListModel` containing all the timeline items.
#[property(get)]
items: gtk::FlattenListModel,
#[property(get = Self::items)]
items: OnceCell<gtk::FlattenListModel>,
/// A Hashmap linking a `TimelineEventItemId` to the corresponding
/// `Event`.
pub(super) event_map: RefCell<HashMap<TimelineEventItemId, Event>>,
@ -95,37 +101,6 @@ mod imp {
read_receipts_changed_handle: OnceCell<AbortHandle>,
}
impl Default for Timeline {
fn default() -> Self {
let start_items = gio::ListStore::new::<TimelineItem>();
let sdk_items = gio::ListStore::new::<TimelineItem>();
let end_items = gio::ListStore::new::<TimelineItem>();
let model_list = gio::ListStore::new::<gio::ListModel>();
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<SdkTimeline> {
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::<TimelineItem>)
}
/// 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::<gio::ListModel>();
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::<VirtualItem>()
.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::<Vec<_>>();
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<TimelineItem> {
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<SdkTimelineItem>;
fn items(&self) -> Vec<TimelineItem> {
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::<TimelineItem>()
.filter_map(|item| item.as_ref().map(item_to_log).ok())
.collect::<Vec<_>>();
@ -852,7 +835,7 @@ mod imp {
fn item_to_log(item: &TimelineItem) -> String {
if let Some(virtual_item) = item.downcast_ref::<VirtualItem>() {
format!("virtual::{:?}", virtual_item.kind().0)
format!("virtual::{:?}", virtual_item.kind())
} else if let Some(event) = item.downcast_ref::<Event>() {
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<OwnedEventId> {
let mut events = vec![];
for item in self.imp().sdk_items.iter::<glib::Object>() {
for item in self.imp().sdk_items().iter::<glib::Object>() {
let Ok(item) = item else {
// The iterator is broken.
break;

113
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<BoxedVirtualItemKind>,
kind: RefCell<VirtualItemKind>,
}
#[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<Vec<Signal>> =
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::<Self>()
.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<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_closure(
"kind-changed",
true,
closure_local!(move |obj: Self| {
f(&obj);
}),
)
}
}

5
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::<glib::Object>();
extra_items.append(&DummyObject::new("add"));
let extra_items = SingleItemListModel::new(&DummyObject::new("add"));
let all_items = gio::ListStore::new::<glib::Object>();
all_items.append(&settings.keywords_list());

25
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<PublicRoomList>,
/// The items added at the end of the list.
end_items: OnceCell<gio::ListStore>,
end_items: OnceCell<SingleItemListModel>,
/// The full list model.
full_model: OnceCell<gio::ListStore>,
}
@ -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::<LoadingRow>)
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 {

29
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<gtk::FlattenListModel>,
/// A model containing a [`LoadingRow`] when the timeline is loading.
loading_item_model: OnceCell<gio::ListStore>,
loading_row: LoadingRow,
loading_item_model: OnceCell<SingleItemListModel>,
}
#[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::<LoadingRow>)
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

56
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<gio::ListModel>,
/// 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<gio::ListStore>,
/// The full list to present for joined members.
#[property(get)]
joined_full: OnceCell<gio::ListModel>,
@ -47,21 +47,6 @@ mod imp {
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";
@ -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::<glib::Object>)
}
/// 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::<gio::ListModel>();
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::<LoadingRow>())
}
@ -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::<LoadingRow>()
let loading_row = if let Some(loading_row) = self
.extra_joined_items()
.item(0)
.and_downcast::<LoadingRow>()
{
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::<MembershipSubpageItem>()
.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();

4
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 => {

35
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<glib::Object>,
item: OnceCell<glib::Object>,
/// Whether the item is hidden.
#[property(get, set = Self::set_is_hidden, explicit_notify)]
is_hidden: Cell<bool>,
}
#[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<glib::Object> {
(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<glib::Object>) -> Self {
glib::Object::builder().property("inner-item", item).build()
glib::Object::builder().property("item", item).build()
}
}

Loading…
Cancel
Save