[fractal] timeline: Use subclass instead of enum for timeline items
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal] timeline: Use subclass instead of enum for timeline items
- Date: Mon, 11 Apr 2022 11:32:05 +0000 (UTC)
commit 8d94e90c5cd2e322c35d71519ab7c411cb640a5d
Author: Kévin Commaille <zecakeh tedomum fr>
Date: Mon Apr 11 13:19:32 2022 +0200
timeline: Use subclass instead of enum for timeline items
Part of #939
po/POTFILES.in | 1 +
src/session/content/room_history/divider_row.rs | 12 +-
src/session/content/room_history/item_row.rs | 179 ++++++-------
src/session/content/room_history/mod.rs | 13 +-
src/session/mod.rs | 2 +-
src/session/room/event.rs | 154 +++++-------
src/session/room/item.rs | 244 ------------------
src/session/room/mod.rs | 13 +-
src/session/room/{timeline.rs => timeline/mod.rs} | 91 ++++---
src/session/room/timeline/timeline_day_divider.rs | 115 +++++++++
src/session/room/timeline/timeline_item.rs | 278 +++++++++++++++++++++
.../room/timeline/timeline_new_messages_divider.rs | 37 +++
src/session/room/timeline/timeline_spinner.rs | 37 +++
13 files changed, 692 insertions(+), 484 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index da8651382..0110d7b5a 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -70,6 +70,7 @@ src/session/mod.rs
src/session/room/event_actions.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_row.rs
diff --git a/src/session/content/room_history/divider_row.rs b/src/session/content/room_history/divider_row.rs
index 538b9b924..f4bc8f110 100644
--- a/src/session/content/room_history/divider_row.rs
+++ b/src/session/content/room_history/divider_row.rs
@@ -77,7 +77,11 @@ glib::wrapper! {
}
impl DividerRow {
- pub fn new(label: String) -> Self {
+ pub fn new() -> Self {
+ glib::Object::new(&[]).expect("Failed to create DividerRow")
+ }
+
+ pub fn with_label(label: String) -> Self {
glib::Object::new(&[("label", &label)]).expect("Failed to create DividerRow")
}
@@ -89,3 +93,9 @@ impl DividerRow {
self.imp().label.text().as_str().to_owned()
}
}
+
+impl Default for DividerRow {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/src/session/content/room_history/item_row.rs b/src/session/content/room_history/item_row.rs
index 8c253ae11..49ac0f3c3 100644
--- a/src/session/content/room_history/item_row.rs
+++ b/src/session/content/room_history/item_row.rs
@@ -7,7 +7,10 @@ use crate::{
components::{ContextMenuBin, ContextMenuBinExt, ContextMenuBinImpl, ReactionChooser},
session::{
content::room_history::{message_row::MessageRow, DividerRow, StateRow},
- room::{Event, EventActions, Item, ItemType},
+ room::{
+ Event, EventActions, TimelineDayDivider, TimelineItem, TimelineNewMessagesDivider,
+ TimelineSpinner,
+ },
},
};
@@ -20,9 +23,10 @@ mod imp {
#[derive(Debug, Default)]
pub struct ItemRow {
- pub item: RefCell<Option<Item>>,
+ pub item: RefCell<Option<TimelineItem>>,
pub menu_model: RefCell<Option<gio::MenuModel>>,
- pub event_notify_handler: RefCell<Option<SignalHandlerId>>,
+ pub notify_handler: RefCell<Option<SignalHandlerId>>,
+ pub binding: RefCell<Option<glib::Binding>>,
pub reaction_chooser: RefCell<Option<ReactionChooser>>,
pub emoji_chooser: RefCell<Option<gtk::EmojiChooser>>,
}
@@ -40,9 +44,9 @@ mod imp {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecObject::new(
"item",
- "item",
- "The item represented by this row",
- Item::static_type(),
+ "Item",
+ "The timeline item represented by this row",
+ TimelineItem::static_type(),
glib::ParamFlags::READWRITE,
)]
});
@@ -58,26 +62,26 @@ mod imp {
pspec: &glib::ParamSpec,
) {
match pspec.name() {
- "item" => {
- let item = value.get::<Option<Item>>().unwrap();
- obj.set_item(item);
- }
+ "item" => obj.set_item(value.get().unwrap()),
_ => unimplemented!(),
}
}
- fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
- "item" => self.item.borrow().to_value(),
+ "item" => obj.item().to_value(),
_ => unimplemented!(),
}
}
fn dispose(&self, _obj: &Self::Type) {
- if let Some(ItemType::Event(event)) =
- self.item.borrow().as_ref().map(|item| item.type_())
+ if let Some(event) = self
+ .item
+ .borrow()
+ .as_ref()
+ .and_then(|item| item.downcast_ref::<Event>())
{
- if let Some(handler) = self.event_notify_handler.borrow_mut().take() {
+ if let Some(handler) = self.notify_handler.borrow_mut().take() {
event.disconnect(handler);
}
}
@@ -101,32 +105,37 @@ impl ItemRow {
glib::Object::new(&[]).expect("Failed to create ItemRow")
}
- /// Get the row's `Item`.
- pub fn item(&self) -> Option<Item> {
+ /// Get the row's [`TimelineItem`].
+ pub fn item(&self) -> Option<TimelineItem> {
self.imp().item.borrow().clone()
}
- /// This method sets this row to a new `Item`.
+ /// This method sets this row to a new [`TimelineItem`].
///
/// It tries to reuse the widget and only update the content whenever
/// possible, but it will create a new widget and drop the old one if it
/// has to.
- fn set_item(&self, item: Option<Item>) {
+ fn set_item(&self, item: Option<TimelineItem>) {
let priv_ = self.imp();
- if let Some(ItemType::Event(event)) = priv_.item.borrow().as_ref().map(|item| item.type_())
+ if let Some(event) = priv_
+ .item
+ .borrow()
+ .as_ref()
+ .and_then(|item| item.downcast_ref::<Event>())
{
- if let Some(handler) = priv_.event_notify_handler.borrow_mut().take() {
+ if let Some(handler) = priv_.notify_handler.borrow_mut().take() {
event.disconnect(handler);
}
+ } else if let Some(binding) = priv_.binding.borrow_mut().take() {
+ binding.unbind()
}
if let Some(ref item) = item {
- match item.type_() {
- ItemType::Event(event) => {
- if event.message_content().is_some() {
- let action_group = self.set_event_actions(Some(event)).unwrap();
- self.set_factory(clone!(@weak event => move |obj, popover| {
+ if let Some(event) = item.downcast_ref::<Event>() {
+ if event.message_content().is_some() {
+ let action_group = self.set_event_actions(Some(event)).unwrap();
+ self.set_factory(clone!(@weak event => move |obj, popover| {
popover.set_menu_model(Some(Self::event_message_menu_model()));
let reaction_chooser = ReactionChooser::new();
reaction_chooser.set_reactions(Some(event.reactions().to_owned()));
@@ -139,72 +148,64 @@ impl ItemRow {
}));
action_group.add_action(&more_reactions);
}));
- } else {
- self.set_factory(|_, popover| {
- popover.set_menu_model(Some(Self::event_state_menu_model()));
- });
- }
-
- let event_notify_handler = event.connect_notify_local(
- Some("event"),
- clone!(@weak self as obj => move |event, _| {
- obj.set_event_widget(event);
- }),
- );
-
- priv_
- .event_notify_handler
- .borrow_mut()
- .replace(event_notify_handler);
-
- self.set_event_widget(event);
- }
- ItemType::DayDivider(date) => {
- self.remove_factory();
- self.set_event_actions(None);
-
- 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")
- };
- let date = date.format(&fmt).unwrap().to_string();
-
- if let Some(Ok(child)) = self.child().map(|w| w.downcast::<DividerRow>()) {
- child.set_label(&date);
- } else {
- let child = DividerRow::new(date);
- self.set_child(Some(&child));
- };
+ } else {
+ self.set_factory(|_, popover| {
+ popover.set_menu_model(Some(Self::event_state_menu_model()));
+ });
}
- ItemType::NewMessageDivider => {
- self.remove_factory();
- self.set_event_actions(None);
- let label = gettext("New Messages");
+ let notify_handler = event.connect_notify_local(
+ Some("event"),
+ clone!(@weak self as obj => move |event, _| {
+ obj.set_event_widget(event);
+ }),
+ );
+ priv_.notify_handler.replace(Some(notify_handler));
+
+ self.set_event_widget(event);
+ } else if let Some(divider) = item.downcast_ref::<TimelineDayDivider>() {
+ self.remove_factory();
+ 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
+ };
- if let Some(Ok(child)) = self.child().map(|w| w.downcast::<DividerRow>()) {
- child.set_label(&label);
- } else {
- let child = DividerRow::new(label);
- self.set_child(Some(&child));
- };
- }
- ItemType::LoadingSpinner => {
- if !self
- .child()
- .map_or(false, |widget| widget.is::<gtk::Spinner>())
- {
- let spinner = gtk::Spinner::builder()
- .spinning(true)
- .margin_top(12)
- .margin_bottom(12)
- .build();
- self.set_child(Some(&spinner));
- }
- }
+ let binding = divider
+ .bind_property("formatted-date", &child, "label")
+ .flags(glib::BindingFlags::SYNC_CREATE)
+ .build();
+ priv_.binding.replace(Some(binding));
+ } else if item.downcast_ref::<TimelineSpinner>().is_some()
+ && self
+ .child()
+ .filter(|widget| widget.is::<gtk::Spinner>())
+ .is_none()
+ {
+ let spinner = gtk::Spinner::builder()
+ .spinning(true)
+ .margin_top(12)
+ .margin_bottom(12)
+ .build();
+ self.set_child(Some(&spinner));
+ } else if item.downcast_ref::<TimelineNewMessagesDivider>().is_some() {
+ self.remove_factory();
+ 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));
+ };
}
}
priv_.item.replace(item);
diff --git a/src/session/content/room_history/mod.rs b/src/session/content/room_history/mod.rs
index 5b98654ed..a5fe8f8f1 100644
--- a/src/session/content/room_history/mod.rs
+++ b/src/session/content/room_history/mod.rs
@@ -30,7 +30,7 @@ use crate::{
components::{CustomEntry, DragOverlay, Pill, RoomTitle},
session::{
content::{MarkdownPopover, RoomDetails},
- room::{Item, Room, RoomType, Timeline, TimelineState},
+ room::{Event, Room, RoomType, Timeline, TimelineState},
user::UserExt,
},
spawn,
@@ -238,15 +238,14 @@ mod imp {
self.listview
.connect_activate(clone!(@weak obj => move |listview, pos| {
- if let Some(item) = listview
+ if let Some(event) = listview
.model()
.and_then(|model| model.item(pos))
- .and_then(|o| o.downcast::<Item>().ok())
+ .as_ref()
+ .and_then(|o| o.downcast_ref::<Event>())
{
- if let Some(event) = item.event() {
- if let Some(room) = obj.room() {
- room.session().show_media(event);
- }
+ if let Some(room) = obj.room() {
+ room.session().show_media(event);
}
}
}));
diff --git a/src/session/mod.rs b/src/session/mod.rs
index cae721659..30b152b33 100644
--- a/src/session/mod.rs
+++ b/src/session/mod.rs
@@ -55,7 +55,7 @@ use self::{
};
pub use self::{
avatar::Avatar,
- room::{Event, Item, Room},
+ room::{Event, Room},
room_creation::RoomCreation,
user::{User, UserActions, UserExt},
};
diff --git a/src/session/room/event.rs b/src/session/room/event.rs
index 27903e006..8252e36c3 100644
--- a/src/session/room/event.rs
+++ b/src/session/room/event.rs
@@ -22,11 +22,12 @@ use matrix_sdk::{
};
use ruma::events::room::member::MembershipState;
+use super::{
+ timeline::{TimelineItem, TimelineItemImpl},
+ Member, ReactionList, Room,
+};
use crate::{
- session::{
- room::{Member, ReactionList},
- Room, UserExt,
- },
+ session::UserExt,
spawn_tokio,
utils::{filename_for_mime, media_type_uid},
};
@@ -36,7 +37,7 @@ use crate::{
pub struct BoxedSyncRoomEvent(SyncRoomEvent);
mod imp {
- use std::cell::{Cell, RefCell};
+ use std::cell::RefCell;
use glib::{object::WeakRef, SignalHandlerId};
use once_cell::{sync::Lazy, unsync::OnceCell};
@@ -54,7 +55,6 @@ mod imp {
pub replacing_events: RefCell<Vec<super::Event>>,
pub reactions: ReactionList,
pub source_changed_handler: RefCell<Option<SignalHandlerId>>,
- pub show_header: Cell<bool>,
pub room: OnceCell<WeakRef<Room>>,
}
@@ -62,6 +62,7 @@ mod imp {
impl ObjectSubclass for Event {
const NAME: &'static str = "RoomEvent";
type Type = super::Event;
+ type ParentType = TimelineItem;
}
impl ObjectImpl for Event {
@@ -82,27 +83,6 @@ mod imp {
None,
glib::ParamFlags::READABLE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
- glib::ParamSpecBoolean::new(
- "show-header",
- "Show Header",
- "Whether this event should show a header. This does nothing if this event doesn’t
have a header. ",
- false,
- glib::ParamFlags::READWRITE,
- ),
- glib::ParamSpecBoolean::new(
- "can-hide-header",
- "Can hide header",
- "Whether this event is allowed to hide its header or not.",
- false,
- glib::ParamFlags::READABLE,
- ),
- glib::ParamSpecObject::new(
- "sender",
- "Sender",
- "The sender of this matrix event",
- Member::static_type(),
- glib::ParamFlags::READABLE,
- ),
glib::ParamSpecObject::new(
"room",
"Room",
@@ -117,13 +97,6 @@ mod imp {
None,
glib::ParamFlags::READABLE,
),
- glib::ParamSpecBoolean::new(
- "can-view-media",
- "Can View Media",
- "Whether this is a media event that can be viewed",
- false,
- glib::ParamFlags::READABLE,
- ),
]
});
@@ -142,10 +115,6 @@ mod imp {
let event = value.get::<BoxedSyncRoomEvent>().unwrap();
obj.set_matrix_pure_event(event.0);
}
- "show-header" => {
- let show_header = value.get().unwrap();
- let _ = obj.set_show_header(show_header);
- }
"room" => {
self.room
.set(value.get::<Room>().unwrap().downgrade())
@@ -158,21 +127,66 @@ mod imp {
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"source" => obj.source().to_value(),
- "sender" => obj.sender().to_value(),
"room" => obj.room().to_value(),
- "show-header" => obj.show_header().to_value(),
- "can-hide-header" => obj.can_hide_header().to_value(),
"time" => obj.time().to_value(),
- "can-view-media" => obj.can_view_media().to_value(),
_ => unimplemented!(),
}
}
}
+
+ impl TimelineItemImpl for Event {
+ fn selectable(&self, _obj: &TimelineItem) -> bool {
+ true
+ }
+
+ fn activatable(&self, obj: &TimelineItem) -> bool {
+ match obj
+ .downcast_ref::<super::Event>()
+ .and_then(|event| event.message_content())
+ {
+ // The event can be activated to open the media viewer if it's an image or a video.
+ Some(AnyMessageLikeEventContent::RoomMessage(message)) => {
+ matches!(
+ message.msgtype,
+ MessageType::Image(_) | MessageType::Video(_)
+ )
+ }
+ _ => false,
+ }
+ }
+
+ fn can_hide_header(&self, obj: &TimelineItem) -> bool {
+ match obj
+ .downcast_ref::<super::Event>()
+ .and_then(|event| event.message_content())
+ {
+ Some(AnyMessageLikeEventContent::RoomMessage(message)) => {
+ matches!(
+ message.msgtype,
+ MessageType::Audio(_)
+ | MessageType::File(_)
+ | MessageType::Image(_)
+ | MessageType::Location(_)
+ | MessageType::Notice(_)
+ | MessageType::Text(_)
+ | MessageType::Video(_)
+ )
+ }
+ Some(AnyMessageLikeEventContent::Sticker(_)) => true,
+ _ => false,
+ }
+ }
+
+ fn sender(&self, obj: &TimelineItem) -> Option<Member> {
+ obj.downcast_ref::<super::Event>()
+ .map(|event| event.room().members().member_by_id(event.matrix_sender()))
+ }
+ }
}
glib::wrapper! {
/// GObject representation of a Matrix room event.
- pub struct Event(ObjectSubclass<imp::Event>);
+ pub struct Event(ObjectSubclass<imp::Event>) @extends TimelineItem;
}
// TODO:
@@ -215,7 +229,7 @@ impl Event {
priv_.pure_event.replace(Some(event));
self.notify("event");
- self.notify("can-view-media");
+ self.notify("activatable");
}
pub fn matrix_sender(&self) -> Arc<UserId> {
@@ -436,19 +450,6 @@ impl Event {
}
}
- pub fn set_show_header(&self, visible: bool) {
- let priv_ = self.imp();
- if priv_.show_header.get() == visible {
- return;
- }
- priv_.show_header.set(visible);
- self.notify("show-header");
- }
-
- pub fn show_header(&self) -> bool {
- self.imp().show_header.get()
- }
-
/// The content of this message.
///
/// Returns `None` if this is not a message.
@@ -459,25 +460,6 @@ impl Event {
}
}
- pub fn can_hide_header(&self) -> bool {
- match self.message_content() {
- Some(AnyMessageLikeEventContent::RoomMessage(message)) => {
- matches!(
- message.msgtype,
- MessageType::Audio(_)
- | MessageType::File(_)
- | MessageType::Image(_)
- | MessageType::Location(_)
- | MessageType::Notice(_)
- | MessageType::Text(_)
- | MessageType::Video(_)
- )
- }
- Some(AnyMessageLikeEventContent::Sticker(_)) => true,
- _ => false,
- }
- }
-
/// Whether this is a replacing `Event`.
///
/// Replacing matrix events are:
@@ -604,13 +586,6 @@ impl Event {
.or_else(|| self.original_content())
}
- pub fn connect_show_header_notify<F: Fn(&Self, &glib::ParamSpec) + 'static>(
- &self,
- f: F,
- ) -> glib::SignalHandlerId {
- self.connect_notify_local(Some("show-header"), f)
- }
-
/// The content of a media message.
///
/// Compatible events:
@@ -689,19 +664,6 @@ impl Event {
panic!("Trying to get the media content of an event of incompatible type");
}
- /// Whether this is a media event that can be viewed.
- pub fn can_view_media(&self) -> bool {
- match self.message_content() {
- Some(AnyMessageLikeEventContent::RoomMessage(message)) => {
- matches!(
- message.msgtype,
- MessageType::Image(_) | MessageType::Video(_)
- )
- }
- _ => false,
- }
- }
-
/// Get the id of the event this `Event` replies to, if any.
pub fn reply_to_id(&self) -> Option<Box<EventId>> {
match self.original_content()? {
diff --git a/src/session/room/mod.rs b/src/session/room/mod.rs
index a05076a8b..d918ec0a8 100644
--- a/src/session/room/mod.rs
+++ b/src/session/room/mod.rs
@@ -1,7 +1,6 @@
mod event;
mod event_actions;
mod highlight_flags;
-mod item;
mod member;
mod member_list;
mod member_role;
@@ -47,14 +46,16 @@ pub use self::{
event::Event,
event_actions::EventActions,
highlight_flags::HighlightFlags,
- item::{Item, ItemType},
member::{Member, Membership},
member_role::MemberRole,
power_levels::{PowerLevel, PowerLevels, RoomAction, POWER_LEVEL_MAX, POWER_LEVEL_MIN},
reaction_group::ReactionGroup,
reaction_list::ReactionList,
room_type::RoomType,
- timeline::{Timeline, TimelineState},
+ timeline::{
+ Timeline, TimelineDayDivider, TimelineItem, TimelineItemExt, TimelineNewMessagesDivider,
+ TimelineSpinner, TimelineState,
+ },
};
use crate::{
components::{Pill, Toast},
@@ -735,8 +736,7 @@ impl Room {
timeline
.item(i)
.as_ref()
- .and_then(|obj| obj.downcast_ref::<Item>())
- .and_then(|item| item.event())
+ .and_then(|obj| obj.downcast_ref::<Event>())
.and_then(|event| {
// The user sent the event so it's the latest read event.
// Necessary because we don't get read receipts for the user's own events.
@@ -838,8 +838,7 @@ impl Room {
if let Some(event) = timeline
.item(i)
.as_ref()
- .and_then(|obj| obj.downcast_ref::<Item>())
- .and_then(|item| item.event())
+ .and_then(|obj| obj.downcast_ref::<Event>())
{
// This is the event corresponding to the read receipt so there's no unread
// messages.
diff --git a/src/session/room/timeline.rs b/src/session/room/timeline/mod.rs
similarity index 93%
rename from src/session/room/timeline.rs
rename to src/session/room/timeline/mod.rs
index 3b413134b..ad14c91b1 100644
--- a/src/session/room/timeline.rs
+++ b/src/session/room/timeline/mod.rs
@@ -1,3 +1,8 @@
+mod timeline_day_divider;
+mod timeline_item;
+mod timeline_new_messages_divider;
+mod timeline_spinner;
+
use std::{
collections::{HashMap, HashSet, VecDeque},
pin::Pin,
@@ -15,11 +20,15 @@ use matrix_sdk::{
},
Error as MatrixError,
};
+pub use timeline_day_divider::TimelineDayDivider;
+pub use timeline_item::{TimelineItem, TimelineItemExt, TimelineItemImpl};
+pub use timeline_new_messages_divider::TimelineNewMessagesDivider;
+pub use timeline_spinner::TimelineSpinner;
use tokio::task::JoinHandle;
use crate::{
session::{
- room::{Event, Item, ItemType, Room},
+ room::{Event, Room},
user::UserExt,
verification::{IdentityVerification, VERIFICATION_CREATION_TIMEOUT},
},
@@ -61,7 +70,7 @@ mod imp {
/// A store to keep track of related events that aren't known
pub relates_to_events: RefCell<HashMap<Box<EventId>, Vec<Box<EventId>>>>,
/// All events shown in the room history
- pub list: RefCell<VecDeque<Item>>,
+ pub list: RefCell<VecDeque<TimelineItem>>,
/// A Hashmap linking `EventId` to corresponding `Event`
pub event_map: RefCell<HashMap<Box<EventId>, Event>>,
/// Maps the temporary `EventId` of the pending Event to the real
@@ -151,11 +160,13 @@ mod imp {
impl ListModelImpl for Timeline {
fn item_type(&self, _list_model: &Self::Type) -> glib::Type {
- Item::static_type()
+ TimelineItem::static_type()
}
+
fn n_items(&self, _list_model: &Self::Type) -> u32 {
self.list.borrow().len() as u32
}
+
fn item(&self, _list_model: &Self::Type, position: u32) -> Option<glib::Object> {
let list = self.list.borrow();
@@ -166,10 +177,10 @@ mod imp {
}
glib::wrapper! {
- /// List of all loaded Events in a room. Implements ListModel.
+ /// List of all loaded items in a room. Implements ListModel.
///
- /// There is no strict message ordering enforced by the Timeline; events
- /// will be appended/prepended to existing events in the order they are
+ /// There is no strict message ordering enforced by the Timeline; items
+ /// will be appended/prepended to existing items in the order they are
/// received by the server.
///
/// This struct additionally keeps track of pending events that have yet to
@@ -200,41 +211,43 @@ impl Timeline {
let mut previous_timestamp = if position > 0 {
list.get(position - 1)
- .and_then(|item| item.event_timestamp())
+ .and_then(|item| item.downcast_ref::<Event>())
+ .map(|event| event.timestamp())
} else {
None
};
- let mut divider: Vec<(usize, Item)> = vec![];
+ let mut dividers: Vec<(usize, TimelineDayDivider)> = vec![];
let mut index = position;
for current in list.range(position..position + added) {
- if let Some(current_timestamp) = current.event_timestamp() {
+ if let Some(current_timestamp) = current
+ .downcast_ref::<Event>()
+ .map(|event| event.timestamp())
+ {
if Some(current_timestamp.ymd()) != previous_timestamp.as_ref().map(|t| t.ymd())
{
- divider.push((index, Item::for_day_divider(current_timestamp.clone())));
+ dividers.push((index, TimelineDayDivider::new(current_timestamp.clone())));
previous_timestamp = Some(current_timestamp);
}
}
index += 1;
}
- let divider_len = divider.len();
- last_new_message_date = divider.last().and_then(|item| match item.1.type_() {
- ItemType::DayDivider(date) => Some(date.clone()),
- _ => None,
- });
- for (added, (position, date)) in divider.into_iter().enumerate() {
- list.insert(position + added, date);
+ let dividers_len = dividers.len();
+ last_new_message_date = dividers.last().and_then(|(_, divider)| divider.date());
+ for (added, (position, date)) in dividers.into_iter().enumerate() {
+ list.insert(position + added, date.upcast());
}
- (added + divider_len) as u32
+ (added + dividers_len) as u32
};
// Remove first day divider if a new one is added earlier with the same day
let removed = {
let mut list = priv_.list.borrow_mut();
- if let Some(ItemType::DayDivider(date)) = list
+ if let Some(date) = list
.get(position as usize + added as usize)
- .map(|item| item.type_())
+ .and_then(|item| item.downcast_ref::<TimelineDayDivider>())
+ .and_then(|divider| divider.date())
{
if Some(date.ymd()) == last_new_message_date.as_ref().map(|date| date.ymd()) {
list.remove(position as usize + added as usize);
@@ -255,14 +268,14 @@ impl Timeline {
let mut previous_sender = if position > 0 {
list.get(position - 1)
- .filter(|event| event.can_hide_header())
- .and_then(|event| event.matrix_sender())
+ .filter(|item| item.can_hide_header())
+ .and_then(|item| item.sender())
} else {
None
};
for current in list.range(position..position + added) {
- let current_sender = current.matrix_sender();
+ let current_sender = current.sender();
if !current.can_hide_header() {
current.set_show_header(false);
@@ -277,7 +290,7 @@ impl Timeline {
// Update the events after the new events
for next in list.range((position + added)..) {
- // After an event with non hiddable header the visibility for headers will be
+ // After an event with non hideable header the visibility for headers will be
// correct
if !next.can_hide_header() {
break;
@@ -285,7 +298,7 @@ impl Timeline {
// Once the sender changes we can be sure that the visibility for headers will
// be correct
- if next.matrix_sender() != previous_sender {
+ if next.sender() != previous_sender {
next.set_show_header(true);
break;
}
@@ -304,7 +317,7 @@ impl Timeline {
for event in list
.range(position as usize..(position + added) as usize)
- .filter_map(|item| item.event())
+ .filter_map(|item| item.downcast_ref::<Event>())
{
if let Some(relates_to) = relates_to_events.remove(&event.matrix_event_id()) {
let mut replacing_events: Vec<Event> = vec![];
@@ -356,7 +369,7 @@ impl Timeline {
let mut list = list.iter();
while let Some(item) = list.next_back() {
- if let ItemType::Event(event) = item.type_() {
+ if let Some(event) = item.downcast_ref::<Event>() {
if redacted_events.remove(&event.matrix_event_id()) {
redacted_events_pos.push(i - 1);
}
@@ -394,15 +407,15 @@ impl Timeline {
// Remove the day divider before this event if it's not useful anymore.
let day_divider_before = pos >= 1
- && matches!(
- list.get(pos - 1).map(|item| item.type_()),
- Some(ItemType::DayDivider(_))
- );
+ && list
+ .get(pos - 1)
+ .filter(|item| item.is::<TimelineDayDivider>())
+ .is_some();
let after = pos == list.len()
- || matches!(
- list.get(pos).map(|item| item.type_()),
- Some(ItemType::DayDivider(_))
- );
+ || list
+ .get(pos)
+ .filter(|item| item.is::<TimelineDayDivider>())
+ .is_some();
if day_divider_before && after {
pos -= 1;
@@ -679,7 +692,7 @@ impl Timeline {
hidden_events.push(event);
added -= 1;
} else {
- priv_.list.borrow_mut().push_back(Item::for_event(event));
+ priv_.list.borrow_mut().push_back(event.upcast());
}
}
}
@@ -714,7 +727,7 @@ impl Timeline {
self.add_hidden_events(vec![event], false);
None
} else {
- list.push_back(Item::for_event(event));
+ list.push_back(event.upcast());
Some(index)
}
};
@@ -782,7 +795,7 @@ impl Timeline {
hidden_events.push(event);
added -= 1;
} else {
- priv_.list.borrow_mut().push_front(Item::for_event(event));
+ priv_.list.borrow_mut().push_front(event.upcast());
}
}
self.add_hidden_events(hidden_events, true);
@@ -826,7 +839,7 @@ impl Timeline {
self.imp()
.list
.borrow_mut()
- .push_front(Item::for_loading_spinner());
+ .push_front(TimelineSpinner::new().upcast());
self.upcast_ref::<gio::ListModel>().items_changed(0, 0, 1);
}
diff --git a/src/session/room/timeline/timeline_day_divider.rs
b/src/session/room/timeline/timeline_day_divider.rs
new file mode 100644
index 000000000..082bbad43
--- /dev/null
+++ b/src/session/room/timeline/timeline_day_divider.rs
@@ -0,0 +1,115 @@
+use gettextrs::gettext;
+use gtk::{glib, prelude::*, subclass::prelude::*};
+
+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::new(
+ "date",
+ "Date",
+ "The date of this divider",
+ glib::DateTime::static_type(),
+ glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+ ),
+ glib::ParamSpecString::new(
+ "formatted-date",
+ "Formatted Date",
+ "The localized representation of the date of this divider",
+ None,
+ glib::ParamFlags::READABLE,
+ ),
+ ]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "date" => obj.set_date(value.get().unwrap()),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "date" => obj.date().to_value(),
+ "formatted-date" => obj.formatted_date().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+ }
+
+ impl TimelineItemImpl for TimelineDayDivider {}
+}
+
+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::new::<Self>(&[("date", &date)]).expect("Failed to create TimelineDayDivider")
+ }
+
+ pub fn date(&self) -> Option<glib::DateTime> {
+ self.imp().date.borrow().clone()
+ }
+
+ pub fn set_date(&self, date: Option<glib::DateTime>) {
+ let priv_ = self.imp();
+
+ if priv_.date.borrow().as_ref() == date.as_ref() {
+ return;
+ }
+
+ priv_.date.replace(date);
+ self.notify("date");
+ self.notify("formatted-date");
+ }
+
+ 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
new file mode 100644
index 000000000..d1c5f8b24
--- /dev/null
+++ b/src/session/room/timeline/timeline_item.rs
@@ -0,0 +1,278 @@
+use gtk::{glib, prelude::*, subclass::prelude::*};
+
+use crate::session::room::Member;
+
+mod imp {
+ use std::cell::Cell;
+
+ use once_cell::sync::Lazy;
+
+ use super::*;
+
+ #[repr(C)]
+ pub struct TimelineItemClass {
+ pub parent_class: glib::object::ObjectClass,
+ pub selectable: fn(&super::TimelineItem) -> bool,
+ pub activatable: fn(&super::TimelineItem) -> bool,
+ pub can_hide_header: fn(&super::TimelineItem) -> bool,
+ pub sender: fn(&super::TimelineItem) -> Option<Member>,
+ }
+
+ unsafe impl ClassStruct for TimelineItemClass {
+ type Type = TimelineItem;
+ }
+
+ pub(super) fn timeline_item_selectable(this: &super::TimelineItem) -> bool {
+ let klass = this.class();
+ (klass.as_ref().selectable)(this)
+ }
+
+ pub(super) fn timeline_item_activatable(this: &super::TimelineItem) -> bool {
+ let klass = this.class();
+ (klass.as_ref().activatable)(this)
+ }
+
+ pub(super) fn timeline_item_can_hide_header(this: &super::TimelineItem) -> bool {
+ let klass = this.class();
+ (klass.as_ref().can_hide_header)(this)
+ }
+
+ pub(super) fn timeline_item_sender(this: &super::TimelineItem) -> Option<Member> {
+ let klass = this.class();
+ (klass.as_ref().sender)(this)
+ }
+
+ #[derive(Debug, Default)]
+ pub struct TimelineItem {
+ pub show_header: Cell<bool>,
+ }
+
+ #[glib::object_subclass]
+ unsafe impl ObjectSubclass for TimelineItem {
+ const NAME: &'static str = "TimelineItem";
+ const ABSTRACT: bool = true;
+ type Type = super::TimelineItem;
+ type Class = TimelineItemClass;
+ }
+
+ impl ObjectImpl for TimelineItem {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![
+ glib::ParamSpecBoolean::new(
+ "selectable",
+ "Selectable",
+ "Whether this item is selectable.",
+ false,
+ glib::ParamFlags::READABLE,
+ ),
+ glib::ParamSpecBoolean::new(
+ "activatable",
+ "Activatable",
+ "Whether this item is activatable.",
+ false,
+ glib::ParamFlags::READABLE,
+ ),
+ glib::ParamSpecBoolean::new(
+ "show-header",
+ "Show Header",
+ "Whether this item should show its header.",
+ false,
+ glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+ ),
+ glib::ParamSpecBoolean::new(
+ "can-hide-header",
+ "Can hide header",
+ "Whether this item is allowed to hide its header.",
+ false,
+ glib::ParamFlags::READABLE,
+ ),
+ glib::ParamSpecObject::new(
+ "sender",
+ "Sender",
+ "If this item is a Matrix event, the sender of the event.",
+ Member::static_type(),
+ glib::ParamFlags::READABLE,
+ ),
+ ]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "show-header" => obj.set_show_header(value.get().unwrap()),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "selectable" => obj.selectable().to_value(),
+ "activatable" => obj.activatable().to_value(),
+ "show-header" => obj.show_header().to_value(),
+ "can-hide-header" => obj.can_hide_header().to_value(),
+ "sender" => obj.sender().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+ }
+}
+
+glib::wrapper! {
+ /// Interface implemented by items inside the `Timeline`.
+ pub struct TimelineItem(ObjectSubclass<imp::TimelineItem>);
+}
+
+/// Public trait containing implemented methods for everything that derives from
+/// `TimelineItem`.
+///
+/// To override the behavior of these methods, override the corresponding method
+/// of `TimelineItemImpl`.
+pub trait TimelineItemExt: 'static {
+ /// Whether this `TimelineItem` is selectable.
+ ///
+ /// Defaults to `false`.
+ fn selectable(&self) -> bool;
+
+ /// Whether this `TimelineItem` is activatable.
+ ///
+ /// Defaults to `false`.
+ fn activatable(&self) -> bool;
+
+ /// Whether this `TimelineItem` should show its header.
+ ///
+ /// Defaults to `false`.
+ fn show_header(&self) -> bool;
+
+ /// Set whether this `TimelineItem` should show its header.
+ fn set_show_header(&self, show: bool);
+
+ /// Whether this `TimelineItem` is allowed to hide its header.
+ ///
+ /// Defaults to `false`.
+ fn can_hide_header(&self) -> bool;
+
+ /// If this is a Matrix event, the sender of the event.
+ ///
+ /// Defaults to `None`.
+ fn sender(&self) -> Option<Member>;
+}
+
+impl<O: IsA<TimelineItem>> TimelineItemExt for O {
+ fn selectable(&self) -> bool {
+ imp::timeline_item_selectable(self.upcast_ref())
+ }
+
+ fn activatable(&self) -> bool {
+ imp::timeline_item_activatable(self.upcast_ref())
+ }
+
+ fn show_header(&self) -> bool {
+ self.upcast_ref().imp().show_header.get()
+ }
+
+ fn set_show_header(&self, show: bool) {
+ let item = self.upcast_ref();
+
+ if item.show_header() == show {
+ return;
+ }
+
+ item.imp().show_header.set(show);
+ item.notify("show-header");
+ }
+
+ fn can_hide_header(&self) -> bool {
+ imp::timeline_item_can_hide_header(self.upcast_ref())
+ }
+
+ fn sender(&self) -> Option<Member> {
+ imp::timeline_item_sender(self.upcast_ref())
+ }
+}
+
+/// Public trait that must be implemented for everything that derives from
+/// `TimelineItem`.
+///
+/// Overriding a method from this Trait overrides also its behavior in
+/// `TimelineItemExt`.
+pub trait TimelineItemImpl: ObjectImpl {
+ fn selectable(&self, _obj: &TimelineItem) -> bool {
+ false
+ }
+
+ fn activatable(&self, _obj: &TimelineItem) -> bool {
+ false
+ }
+
+ fn can_hide_header(&self, _obj: &TimelineItem) -> bool {
+ false
+ }
+
+ fn sender(&self, _obj: &TimelineItem) -> Option<Member> {
+ None
+ }
+}
+
+// Make `TimelineItem` subclassable.
+unsafe impl<T> IsSubclassable<T> for TimelineItem
+where
+ T: TimelineItemImpl,
+ T::Type: IsA<TimelineItem>,
+{
+ fn class_init(class: &mut glib::Class<Self>) {
+ Self::parent_class_init::<T>(class.upcast_ref_mut());
+
+ let klass = class.as_mut();
+
+ klass.selectable = selectable_trampoline::<T>;
+ klass.activatable = activatable_trampoline::<T>;
+ klass.can_hide_header = can_hide_header_trampoline::<T>;
+ klass.sender = sender_trampoline::<T>;
+ }
+}
+
+// Virtual method implementation trampolines.
+fn selectable_trampoline<T>(this: &TimelineItem) -> bool
+where
+ T: ObjectSubclass + TimelineItemImpl,
+ T::Type: IsA<TimelineItem>,
+{
+ let imp = this.downcast_ref::<T::Type>().unwrap().imp();
+ imp.selectable(this)
+}
+
+fn activatable_trampoline<T>(this: &TimelineItem) -> bool
+where
+ T: ObjectSubclass + TimelineItemImpl,
+ T::Type: IsA<TimelineItem>,
+{
+ let imp = this.downcast_ref::<T::Type>().unwrap().imp();
+ imp.activatable(this)
+}
+
+fn can_hide_header_trampoline<T>(this: &TimelineItem) -> bool
+where
+ T: ObjectSubclass + TimelineItemImpl,
+ T::Type: IsA<TimelineItem>,
+{
+ let imp = this.downcast_ref::<T::Type>().unwrap().imp();
+ imp.can_hide_header(this)
+}
+
+fn sender_trampoline<T>(this: &TimelineItem) -> Option<Member>
+where
+ T: ObjectSubclass + TimelineItemImpl,
+ T::Type: IsA<TimelineItem>,
+{
+ let imp = this.downcast_ref::<T::Type>().unwrap().imp();
+ imp.sender(this)
+}
diff --git a/src/session/room/timeline/timeline_new_messages_divider.rs
b/src/session/room/timeline/timeline_new_messages_divider.rs
new file mode 100644
index 000000000..f6862153e
--- /dev/null
+++ b/src/session/room/timeline/timeline_new_messages_divider.rs
@@ -0,0 +1,37 @@
+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 {}
+}
+
+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(&[]).expect("Failed to create TimelineNewMessagesDivider")
+ }
+}
+
+impl Default for TimelineNewMessagesDivider {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/src/session/room/timeline/timeline_spinner.rs b/src/session/room/timeline/timeline_spinner.rs
new file mode 100644
index 000000000..153537b10
--- /dev/null
+++ b/src/session/room/timeline/timeline_spinner.rs
@@ -0,0 +1,37 @@
+use gtk::{glib, subclass::prelude::*};
+
+use super::{TimelineItem, TimelineItemImpl};
+
+mod imp {
+ use super::*;
+
+ #[derive(Debug, Default)]
+ pub struct TimelineSpinner;
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for TimelineSpinner {
+ const NAME: &'static str = "TimelineSpinner";
+ type Type = super::TimelineSpinner;
+ type ParentType = TimelineItem;
+ }
+
+ impl ObjectImpl for TimelineSpinner {}
+ impl TimelineItemImpl for TimelineSpinner {}
+}
+
+glib::wrapper! {
+ /// A loading spinner in the timeline.
+ pub struct TimelineSpinner(ObjectSubclass<imp::TimelineSpinner>) @extends TimelineItem;
+}
+
+impl TimelineSpinner {
+ pub fn new() -> Self {
+ glib::Object::new(&[]).expect("Failed to create TimelineSpinner")
+ }
+}
+
+impl Default for TimelineSpinner {
+ fn default() -> Self {
+ Self::new()
+ }
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]