timeline: Group virtual items in a single type

Now that the SDK timeline API handles most of the logic, there is just a
lot of boilerplate code for little to no gain.
This commit is contained in:
Kévin Commaille 2023-04-24 11:44:05 +02:00
parent cfeae158ad
commit 2a07a1b67c
No known key found for this signature in database
GPG key ID: DD507DAE96E8245C
8 changed files with 205 additions and 347 deletions

View file

@ -91,7 +91,6 @@ src/session/room/event/event_actions.rs
src/session/room/member.rs
src/session/room/member_role.rs
src/session/room/mod.rs
src/session/room/timeline/timeline_day_divider.rs
src/session/room_creation/mod.rs
src/session/room_list.rs
src/session/sidebar/category/category_row.rs

View file

@ -9,10 +9,7 @@ use crate::{
content::room_history::{
message_row::MessageRow, DividerRow, RoomHistory, StateRow, TypingRow,
},
room::{
Event, EventActions, EventTexture, PlaceholderKind, TimelineDayDivider, TimelineItem,
TimelineNewMessagesDivider, TimelinePlaceholder,
},
room::{Event, EventActions, EventTexture, TimelineItem, VirtualItem, VirtualItemKind},
},
utils::BoundObjectWeakRef,
};
@ -270,49 +267,21 @@ impl ItemRow {
self.set_event_widget(event);
self.set_action_group(self.set_event_actions(Some(event.upcast_ref())));
} else if let Some(divider) = item.downcast_ref::<TimelineDayDivider>() {
} else if let Some(item) = item.downcast_ref::<VirtualItem>() {
self.set_popover(None);
self.set_action_group(None);
self.set_event_actions(None);
let child = if let Some(child) =
self.child().and_then(|w| w.downcast::<DividerRow>().ok())
{
child
} else {
let child = DividerRow::new();
self.set_child(Some(&child));
child
};
let binding = divider
.bind_property("formatted-date", &child, "label")
.flags(glib::BindingFlags::SYNC_CREATE)
.build();
imp.binding.replace(Some(binding));
} else if let Some(item) = item.downcast_ref::<TimelinePlaceholder>() {
match item.kind() {
PlaceholderKind::Spinner => {
if self
.child()
.filter(|widget| widget.is::<Spinner>())
.is_none()
{
self.set_popover(None);
self.set_action_group(None);
self.set_event_actions(None);
VirtualItemKind::Spinner => {
if !self.child().map_or(false, |widget| widget.is::<Spinner>()) {
let spinner = Spinner::default();
spinner.set_margin_top(12);
spinner.set_margin_bottom(12);
self.set_child(Some(&spinner));
}
}
PlaceholderKind::Typing => {
self.set_popover(None);
self.set_action_group(None);
self.set_event_actions(None);
VirtualItemKind::Typing => {
let child = if let Some(child) =
self.child().and_then(|w| w.downcast::<TypingRow>().ok())
{
@ -330,13 +299,41 @@ impl ItemRow {
.map(|room| room.typing_list()),
);
}
PlaceholderKind::TimelineStart => {
self.set_popover(None);
self.set_action_group(None);
self.set_event_actions(None);
VirtualItemKind::TimelineStart => {
let label = gettext("This is the start of the visible history");
if let Some(Ok(child)) = self.child().map(|w| w.downcast::<DividerRow>()) {
child.set_label(&label);
} else {
let child = DividerRow::with_label(label);
self.set_child(Some(&child));
};
}
VirtualItemKind::DayDivider(date) => {
let child = if let Some(child) =
self.child().and_then(|w| w.downcast::<DividerRow>().ok())
{
child
} else {
let child = DividerRow::new();
self.set_child(Some(&child));
child
};
let fmt = if date.year() == glib::DateTime::now_local().unwrap().year() {
// Translators: This is a date format in the day divider without the
// year
gettext("%A, %B %e")
} else {
// Translators: This is a date format in the day divider with the year
gettext("%A, %B %e, %Y")
};
child.set_label(&date.format(&fmt).unwrap())
}
VirtualItemKind::NewMessages => {
let label = gettext("New Messages");
if let Some(Ok(child)) = self.child().map(|w| w.downcast::<DividerRow>()) {
child.set_label(&label);
} else {
@ -345,19 +342,6 @@ impl ItemRow {
};
}
}
} else if item.downcast_ref::<TimelineNewMessagesDivider>().is_some() {
self.set_popover(None);
self.set_action_group(None);
self.set_event_actions(None);
let label = gettext("New Messages");
if let Some(Ok(child)) = self.child().map(|w| w.downcast::<DividerRow>()) {
child.set_label(&label);
} else {
let child = DividerRow::with_label(label);
self.set_child(Some(&child));
};
}
}
imp.item.replace(item);

View file

@ -1,7 +1,5 @@
mod timeline_day_divider;
mod timeline_item;
mod timeline_new_messages_divider;
mod timeline_placeholder;
mod virtual_item;
use std::{
collections::{HashMap, VecDeque},
@ -18,11 +16,11 @@ use matrix_sdk::{
Error as MatrixError,
};
use ruma::events::AnySyncTimelineEvent;
pub use timeline_day_divider::TimelineDayDivider;
pub use timeline_item::{TimelineItem, TimelineItemExt, TimelineItemImpl};
pub use timeline_new_messages_divider::TimelineNewMessagesDivider;
pub use timeline_placeholder::{PlaceholderKind, TimelinePlaceholder};
pub use self::{
timeline_item::{TimelineItem, TimelineItemExt, TimelineItemImpl},
virtual_item::{VirtualItem, VirtualItemKind},
};
use super::{Event, EventKey, Room};
use crate::{spawn, spawn_tokio};
@ -158,7 +156,7 @@ impl Timeline {
let imp = self.imp();
if imp.has_typing.get() && position == self.n_items_in_list() {
return Some(TimelinePlaceholder::typing().upcast());
return Some(VirtualItem::typing().upcast());
}
imp.list
@ -387,9 +385,8 @@ impl Timeline {
if let Some(event) = item.downcast_ref::<Event>() {
self.imp().event_map.borrow_mut().remove(&event.key());
} else if item
.downcast_ref::<TimelinePlaceholder>()
.filter(|item| item.kind() == PlaceholderKind::Spinner)
.is_some()
.downcast_ref::<VirtualItem>()
.map_or(false, |item| item.kind() == VirtualItemKind::Spinner)
&& self.state() == TimelineState::Loading
{
self.set_state(TimelineState::Ready)

View file

@ -1,130 +0,0 @@
use gettextrs::gettext;
use gtk::{glib, prelude::*, subclass::prelude::*};
use ruma::MilliSecondsSinceUnixEpoch;
use super::{TimelineItem, TimelineItemImpl};
mod imp {
use std::cell::RefCell;
use once_cell::sync::Lazy;
use super::*;
#[derive(Debug, Default)]
pub struct TimelineDayDivider {
/// The date of this divider.
pub date: RefCell<Option<glib::DateTime>>,
}
#[glib::object_subclass]
impl ObjectSubclass for TimelineDayDivider {
const NAME: &'static str = "TimelineDayDivider";
type Type = super::TimelineDayDivider;
type ParentType = TimelineItem;
}
impl ObjectImpl for TimelineDayDivider {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecBoxed::builder::<glib::DateTime>("date")
.explicit_notify()
.build(),
glib::ParamSpecString::builder("formatted-date")
.read_only()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"date" => self.obj().set_date(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"date" => obj.date().to_value(),
"formatted-date" => obj.formatted_date().to_value(),
_ => unimplemented!(),
}
}
}
impl TimelineItemImpl for TimelineDayDivider {
fn id(&self) -> String {
format!(
"TimelineDayDivider::{}",
self.obj()
.date()
.map(|d| d.format("%F").unwrap())
.unwrap_or_default()
)
}
}
}
glib::wrapper! {
/// A day divider in the timeline.
pub struct TimelineDayDivider(ObjectSubclass<imp::TimelineDayDivider>) @extends TimelineItem;
}
impl TimelineDayDivider {
pub fn new(date: glib::DateTime) -> Self {
glib::Object::builder().property("date", &date).build()
}
/// Creates a new `TimelineDayDivider` for the given timestamp.
///
/// If the timestamp is out of range for `glib::DateTime` (later than the
/// end of year 9999), this fallbacks to creating a divider with the
/// current local time.
///
/// Panics if an error occurred when accessing the current local time.
pub fn with_timestamp(timestamp: MilliSecondsSinceUnixEpoch) -> Self {
let date = glib::DateTime::from_unix_utc(timestamp.as_secs().into())
.expect("The day divider timestamp should be before year 10,000");
Self::new(date)
}
/// The date of this divider.
pub fn date(&self) -> Option<glib::DateTime> {
self.imp().date.borrow().clone()
}
/// Set the date of this divider.
pub fn set_date(&self, date: Option<glib::DateTime>) {
let imp = self.imp();
if imp.date.borrow().as_ref() == date.as_ref() {
return;
}
imp.date.replace(date);
self.notify("date");
self.notify("formatted-date");
}
/// The localized representation of the date of this divider.
pub fn formatted_date(&self) -> String {
self.date()
.map(|date| {
let fmt = if date.year() == glib::DateTime::now_local().unwrap().year() {
// Translators: This is a date format in the day divider without the year
gettext("%A, %B %e")
} else {
// Translators: This is a date format in the day divider with the year
gettext("%A, %B %e, %Y")
};
date.format(&fmt).unwrap().to_string()
})
.unwrap_or_default()
}
}

View file

@ -1,7 +1,7 @@
use gtk::{glib, prelude::*, subclass::prelude::*};
use matrix_sdk::room::timeline::{TimelineItem as SdkTimelineItem, VirtualTimelineItem};
use matrix_sdk::room::timeline::TimelineItem as SdkTimelineItem;
use super::{TimelineDayDivider, TimelineNewMessagesDivider, TimelinePlaceholder};
use super::VirtualItem;
use crate::session::{
room::{Event, Member},
Room,
@ -124,20 +124,8 @@ impl TimelineItem {
/// Constructs the proper child type.
pub fn new(item: &SdkTimelineItem, room: &Room) -> Self {
match item {
SdkTimelineItem::Event(event) => {
let event = Event::new(event.clone(), room);
event.upcast()
}
SdkTimelineItem::Virtual(item) => match item {
VirtualTimelineItem::DayDivider(ts) => {
TimelineDayDivider::with_timestamp(*ts).upcast()
}
VirtualTimelineItem::ReadMarker => TimelineNewMessagesDivider::new().upcast(),
VirtualTimelineItem::LoadingIndicator => TimelinePlaceholder::spinner().upcast(),
VirtualTimelineItem::TimelineStart => {
TimelinePlaceholder::timeline_start().upcast()
}
},
SdkTimelineItem::Event(event) => Event::new(event.clone(), room).upcast(),
SdkTimelineItem::Virtual(item) => VirtualItem::new(item).upcast(),
}
}

View file

@ -1,36 +0,0 @@
use gtk::{glib, subclass::prelude::*};
use super::{TimelineItem, TimelineItemImpl};
mod imp {
use super::*;
#[derive(Debug, Default)]
pub struct TimelineNewMessagesDivider;
#[glib::object_subclass]
impl ObjectSubclass for TimelineNewMessagesDivider {
const NAME: &'static str = "TimelineNewMessagesDivider";
type Type = super::TimelineNewMessagesDivider;
type ParentType = TimelineItem;
}
impl ObjectImpl for TimelineNewMessagesDivider {}
impl TimelineItemImpl for TimelineNewMessagesDivider {
fn id(&self) -> String {
"TimelineNewMessagesDivider".to_owned()
}
}
}
glib::wrapper! {
/// A divider for the read marker in the timeline.
pub struct TimelineNewMessagesDivider(ObjectSubclass<imp::TimelineNewMessagesDivider>) @extends TimelineItem;
}
impl TimelineNewMessagesDivider {
pub fn new() -> Self {
glib::Object::new()
}
}

View file

@ -1,99 +0,0 @@
use gtk::{glib, prelude::*, subclass::prelude::*};
use super::{TimelineItem, TimelineItemImpl};
#[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
#[repr(u32)]
#[enum_type(name = "PlaceholderKind")]
pub enum PlaceholderKind {
#[default]
Spinner = 0,
Typing = 1,
TimelineStart = 2,
}
mod imp {
use std::cell::Cell;
use once_cell::sync::Lazy;
use super::*;
#[derive(Debug, Default)]
pub struct TimelinePlaceholder {
/// The kind of placeholder.
pub kind: Cell<PlaceholderKind>,
}
#[glib::object_subclass]
impl ObjectSubclass for TimelinePlaceholder {
const NAME: &'static str = "TimelinePlaceholder";
type Type = super::TimelinePlaceholder;
type ParentType = TimelineItem;
}
impl ObjectImpl for TimelinePlaceholder {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecEnum::builder::<PlaceholderKind>("kind")
.construct_only()
.build()]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"kind" => self.kind.set(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"kind" => self.kind.get().to_value(),
_ => unimplemented!(),
}
}
}
impl TimelineItemImpl for TimelinePlaceholder {
fn id(&self) -> String {
match self.obj().kind() {
PlaceholderKind::Spinner => "TimelinePlaceholder::Spinner",
PlaceholderKind::Typing => "TimelinePlaceholder::Typing",
PlaceholderKind::TimelineStart => "TimelinePlaceholder::TimelineStart",
}
.to_owned()
}
}
}
glib::wrapper! {
/// A loading spinner in the timeline.
pub struct TimelinePlaceholder(ObjectSubclass<imp::TimelinePlaceholder>) @extends TimelineItem;
}
impl TimelinePlaceholder {
pub fn spinner() -> Self {
glib::Object::new()
}
pub fn typing() -> Self {
glib::Object::builder()
.property("kind", PlaceholderKind::Typing)
.build()
}
pub fn timeline_start() -> Self {
glib::Object::builder()
.property("kind", PlaceholderKind::TimelineStart)
.build()
}
/// The kind of placeholder.
pub fn kind(&self) -> PlaceholderKind {
self.imp().kind.get()
}
}

View file

@ -0,0 +1,155 @@
use gtk::{glib, prelude::*, subclass::prelude::*};
use matrix_sdk::room::timeline::VirtualTimelineItem;
use ruma::MilliSecondsSinceUnixEpoch;
use super::{TimelineItem, TimelineItemImpl};
#[derive(Debug, Default, Eq, PartialEq, Clone)]
pub enum VirtualItemKind {
#[default]
Spinner,
Typing,
TimelineStart,
DayDivider(glib::DateTime),
NewMessages,
}
impl VirtualItemKind {
/// Convert this into a [`VirtualItemKindBoxed`].
fn boxed(self) -> VirtualItemKindBoxed {
VirtualItemKindBoxed(self)
}
}
#[derive(Clone, Debug, PartialEq, Eq, glib::Boxed)]
#[boxed_type(name = "VirtualItemKindBoxed")]
struct VirtualItemKindBoxed(VirtualItemKind);
mod imp {
use std::cell::RefCell;
use once_cell::sync::Lazy;
use super::*;
#[derive(Debug, Default)]
pub struct VirtualItem {
/// The kind of virtual item.
pub kind: RefCell<VirtualItemKind>,
}
#[glib::object_subclass]
impl ObjectSubclass for VirtualItem {
const NAME: &'static str = "TimelineVirtualItem";
type Type = super::VirtualItem;
type ParentType = TimelineItem;
}
impl ObjectImpl for VirtualItem {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecBoxed::builder::<VirtualItemKindBoxed>("kind")
.construct()
.write_only()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"kind" => {
let boxed = value.get::<VirtualItemKindBoxed>().unwrap();
self.kind.replace(boxed.0);
}
_ => unimplemented!(),
}
}
}
impl TimelineItemImpl for VirtualItem {
fn id(&self) -> String {
match self.obj().kind() {
VirtualItemKind::Spinner => "VirtualItem::Spinner".to_owned(),
VirtualItemKind::Typing => "VirtualItem::Typing".to_owned(),
VirtualItemKind::TimelineStart => "VirtualItem::TimelineStart".to_owned(),
VirtualItemKind::DayDivider(date) => {
format!("VirtualItem::DayDivider({})", date.format("%F").unwrap())
}
VirtualItemKind::NewMessages => "VirtualItem::NewMessages".to_owned(),
}
}
}
}
glib::wrapper! {
/// A virtual item in the timeline.
///
/// A virtual item is an item not based on a timeline event.
pub struct VirtualItem(ObjectSubclass<imp::VirtualItem>) @extends TimelineItem;
}
impl VirtualItem {
/// Create a new `VirtualItem` from a virtual timeline item.
pub fn new(item: &VirtualTimelineItem) -> Self {
match item {
VirtualTimelineItem::DayDivider(ts) => Self::day_divider_with_timestamp(*ts),
VirtualTimelineItem::ReadMarker => Self::new_messages(),
VirtualTimelineItem::LoadingIndicator => Self::spinner(),
VirtualTimelineItem::TimelineStart => Self::timeline_start(),
}
}
/// Create a spinner virtual item.
pub fn spinner() -> Self {
glib::Object::builder()
.property("kind", VirtualItemKind::Spinner.boxed())
.build()
}
/// Create a typing virtual item.
pub fn typing() -> Self {
glib::Object::builder()
.property("kind", VirtualItemKind::Typing.boxed())
.build()
}
/// Create a timeline start virtual item.
pub fn timeline_start() -> Self {
glib::Object::builder()
.property("kind", VirtualItemKind::TimelineStart.boxed())
.build()
}
/// Create a new messages virtual item.
pub fn new_messages() -> Self {
glib::Object::builder()
.property("kind", VirtualItemKind::NewMessages.boxed())
.build()
}
/// Creates a new day divider virtual item for the given timestamp.
///
/// If the timestamp is out of range for `glib::DateTime` (later than the
/// end of year 9999), this fallbacks to creating a divider with the
/// current local time.
///
/// Panics if an error occurred when accessing the current local time.
pub fn day_divider_with_timestamp(timestamp: MilliSecondsSinceUnixEpoch) -> Self {
let date = glib::DateTime::from_unix_utc(timestamp.as_secs().into())
.or_else(|_| glib::DateTime::now_local())
.expect("We should be able to get the current time");
glib::Object::builder()
.property("kind", VirtualItemKind::DayDivider(date).boxed())
.build()
}
/// The kind of virtual item.
pub fn kind(&self) -> VirtualItemKind {
self.imp().kind.borrow().clone()
}
}