[fractal] timeline: Use subclass instead of enum for timeline items



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]