diff --git a/po/POTFILES.in b/po/POTFILES.in index 383aeae5..92c6e09b 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -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 diff --git a/src/session/content/room_history/item_row.rs b/src/session/content/room_history/item_row.rs index ea1f7425..05bb832b 100644 --- a/src/session/content/room_history/item_row.rs +++ b/src/session/content/room_history/item_row.rs @@ -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::() { + } else if let Some(item) = item.downcast_ref::() { 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::().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::() { match item.kind() { - PlaceholderKind::Spinner => { - if self - .child() - .filter(|widget| widget.is::()) - .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::()) { 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::().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::()) { + 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::().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::()) { child.set_label(&label); } else { @@ -345,19 +342,6 @@ impl ItemRow { }; } } - } else if item.downcast_ref::().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::()) { - child.set_label(&label); - } else { - let child = DividerRow::with_label(label); - self.set_child(Some(&child)); - }; } } imp.item.replace(item); diff --git a/src/session/room/timeline/mod.rs b/src/session/room/timeline/mod.rs index cb8c1d7f..d6e1a1a1 100644 --- a/src/session/room/timeline/mod.rs +++ b/src/session/room/timeline/mod.rs @@ -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::() { self.imp().event_map.borrow_mut().remove(&event.key()); } else if item - .downcast_ref::() - .filter(|item| item.kind() == PlaceholderKind::Spinner) - .is_some() + .downcast_ref::() + .map_or(false, |item| item.kind() == VirtualItemKind::Spinner) && self.state() == TimelineState::Loading { self.set_state(TimelineState::Ready) diff --git a/src/session/room/timeline/timeline_day_divider.rs b/src/session/room/timeline/timeline_day_divider.rs deleted file mode 100644 index 2fadb0f1..00000000 --- a/src/session/room/timeline/timeline_day_divider.rs +++ /dev/null @@ -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>, - } - - #[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> = Lazy::new(|| { - vec![ - glib::ParamSpecBoxed::builder::("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) @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 { - self.imp().date.borrow().clone() - } - - /// Set the date of this divider. - pub fn set_date(&self, date: Option) { - 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() - } -} diff --git a/src/session/room/timeline/timeline_item.rs b/src/session/room/timeline/timeline_item.rs index 255c47b8..c9386e71 100644 --- a/src/session/room/timeline/timeline_item.rs +++ b/src/session/room/timeline/timeline_item.rs @@ -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(), } } diff --git a/src/session/room/timeline/timeline_new_messages_divider.rs b/src/session/room/timeline/timeline_new_messages_divider.rs deleted file mode 100644 index baf106da..00000000 --- a/src/session/room/timeline/timeline_new_messages_divider.rs +++ /dev/null @@ -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) @extends TimelineItem; -} - -impl TimelineNewMessagesDivider { - pub fn new() -> Self { - glib::Object::new() - } -} diff --git a/src/session/room/timeline/timeline_placeholder.rs b/src/session/room/timeline/timeline_placeholder.rs deleted file mode 100644 index 563c66df..00000000 --- a/src/session/room/timeline/timeline_placeholder.rs +++ /dev/null @@ -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, - } - - #[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> = Lazy::new(|| { - vec![glib::ParamSpecEnum::builder::("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) @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() - } -} diff --git a/src/session/room/timeline/virtual_item.rs b/src/session/room/timeline/virtual_item.rs new file mode 100644 index 00000000..b5b6ba50 --- /dev/null +++ b/src/session/room/timeline/virtual_item.rs @@ -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, + } + + #[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> = Lazy::new(|| { + vec![ + glib::ParamSpecBoxed::builder::("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::().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) @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() + } +}