From 4372f80a6b1e634679b43ae77d2ebb665c3e8796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Sun, 13 Apr 2025 11:35:02 +0200 Subject: [PATCH] room-history: Replace ItemRow by more specific widgets It is now basically the new EventRow, since only events have a context menu. All the widgets that can be direct children of GtkListItem use the `room-history-row` class for CSS styling. --- data/resources/stylesheet/_room_history.scss | 10 +- po/POTFILES.in | 6 +- src/session/model/room/timeline/event/mod.rs | 24 +- .../view/content/room_history/divider_row.rs | 63 +++- .../view/content/room_history/divider_row.ui | 3 + .../{item_row.rs => event_row.rs} | 327 ++++++------------ ...text_menu.rs => event_row_context_menu.rs} | 16 +- ...text_menu.ui => event_row_context_menu.ui} | 0 .../room_history/message_row/visual_media.rs | 7 +- .../message_toolbar/composer_state.rs | 2 +- src/session/view/content/room_history/mod.rs | 122 +++++-- .../view/content/room_history/typing_row.rs | 2 +- .../view/content/room_history/typing_row.ui | 3 + src/ui-resources.gresource.xml | 2 +- 14 files changed, 294 insertions(+), 293 deletions(-) rename src/session/view/content/room_history/{item_row.rs => event_row.rs} (78%) rename src/session/view/content/room_history/{item_row_context_menu.rs => event_row_context_menu.rs} (96%) rename src/session/view/content/room_history/{event_context_menu.ui => event_row_context_menu.ui} (100%) diff --git a/data/resources/stylesheet/_room_history.scss b/data/resources/stylesheet/_room_history.scss index c966bc4c..37662f30 100644 --- a/data/resources/stylesheet/_room_history.scss +++ b/data/resources/stylesheet/_room_history.scss @@ -13,7 +13,7 @@ } } -room-history-row { +.room-history-row { padding-top: 2px; padding-bottom: 2px; padding-left: 8px; @@ -114,7 +114,7 @@ room-history-row { } } -room-history-row .event-content .quote, +.room-history-row .event-content .quote, .related-event-content { border-left: 2px solid var(--accent-bg-color); padding-left: 6px; @@ -238,7 +238,7 @@ typing-row { min-height: 30px; } -room-history-row, .related-event-content { +.room-history-row, .related-event-content { .h1 { font-weight: 800; font-size: 15pt; @@ -270,7 +270,7 @@ room-history-row, .related-event-content { } } -room-history-row expander-widget > box > { +.room-history-row expander-widget > box > { title { border-spacing: 6px; } @@ -311,7 +311,7 @@ room-title { sender-avatar { padding: 3px; border-radius: 100%; - + @include vendor.focus-ring(); &:hover { diff --git a/po/POTFILES.in b/po/POTFILES.in index 0e249869..b479596a 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -143,9 +143,9 @@ src/session/view/content/room_details/permissions/permissions_subpage.rs src/session/view/content/room_details/permissions/permissions_subpage.ui src/session/view/content/room_details/room_upgrade_dialog.rs src/session/view/content/room_history/divider_row.rs -src/session/view/content/room_history/event_context_menu.ui -src/session/view/content/room_history/item_row.rs -src/session/view/content/room_history/item_row_context_menu.rs +src/session/view/content/room_history/event_row.rs +src/session/view/content/room_history/event_row_context_menu.rs +src/session/view/content/room_history/event_row_context_menu.ui src/session/view/content/room_history/message_row/audio.rs src/session/view/content/room_history/message_row/content.rs src/session/view/content/room_history/message_row/file.rs diff --git a/src/session/model/room/timeline/event/mod.rs b/src/session/model/room/timeline/event/mod.rs index eab7653d..a6690db1 100644 --- a/src/session/model/room/timeline/event/mod.rs +++ b/src/session/model/room/timeline/event/mod.rs @@ -575,18 +575,6 @@ impl Event { } } - /// Whether this event contains a message. - /// - /// This definition matches the `m.room.message` event type. - pub(crate) fn is_message(&self) -> bool { - match self.item().content() { - TimelineItemContent::MsgLike(msg_like) => { - matches!(msg_like.kind, MsgLikeKind::Message(_)) - } - _ => false, - } - } - /// Whether this event contains a message-like content. /// /// This definition matches the following event types: @@ -688,13 +676,15 @@ impl Event { /// Whether this event can be replied to. pub(crate) fn can_be_replied_to(&self) -> bool { - // We only allow to reply to messages. - if !self.is_message() { + let item = self.item(); + + // We only allow to reply to messages (but not stickers). + if !item.content().is_message() { return false; } // The SDK API has its own rules. - if !self.item().can_be_replied_to() { + if !item.can_be_replied_to() { return false; } @@ -704,8 +694,8 @@ impl Event { /// Whether this event can be reacted to. pub(crate) fn can_be_reacted_to(&self) -> bool { - // We only allow to react to messages. - if !self.is_message() { + // We only allow to react to messages (but not stickers). + if !self.item().content().is_message() { return false; } diff --git a/src/session/view/content/room_history/divider_row.rs b/src/session/view/content/room_history/divider_row.rs index 8d60504e..b1e0eb30 100644 --- a/src/session/view/content/room_history/divider_row.rs +++ b/src/session/view/content/room_history/divider_row.rs @@ -1,19 +1,26 @@ -use adw::subclass::prelude::*; +use adw::{prelude::*, subclass::prelude::*}; use gettextrs::gettext; -use gtk::{glib, prelude::*, CompositeTemplate}; +use gtk::{glib, glib::clone, CompositeTemplate}; -use crate::session::model::VirtualItemKind; +use crate::{ + session::model::{VirtualItem, VirtualItemKind}, + utils::BoundObject, +}; mod imp { use glib::subclass::InitializingObject; use super::*; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template(resource = "/org/gnome/Fractal/ui/session/view/content/room_history/divider_row.ui")] + #[properties(wrapper_type = super::DividerRow)] pub struct DividerRow { #[template_child] inner_label: TemplateChild, + /// The virtual item presented by this row. + #[property(get, set = Self::set_virtual_item, explicit_notify, nullable)] + virtual_item: BoundObject, } #[glib::object_subclass] @@ -26,6 +33,7 @@ mod imp { Self::bind_template(klass); klass.set_css_name("divider-row"); + klass.set_accessible_role(gtk::AccessibleRole::ListItem); } fn instance_init(obj: &InitializingObject) { @@ -33,18 +41,51 @@ mod imp { } } + #[glib::derived_properties] impl ObjectImpl for DividerRow {} impl WidgetImpl for DividerRow {} impl BinImpl for DividerRow {} impl DividerRow { - /// Set the kind of this divider. + /// Set the virtual item presented by this row. + fn set_virtual_item(&self, virtual_item: Option) { + if self.virtual_item.obj() == virtual_item { + return; + } + + self.virtual_item.disconnect_signals(); + + if let Some(virtual_item) = virtual_item { + let kind_handler = virtual_item.connect_kind_changed(clone!( + #[weak(rename_to = imp)] + self, + move |_| { + imp.update(); + } + )); + + self.virtual_item.set(virtual_item, vec![kind_handler]); + } + + self.update(); + self.obj().notify_virtual_item(); + } + + /// Update this row for the current kind. /// /// Panics if the kind is not `TimelineStart`, `DayDivider` or /// `NewMessages`. - pub(super) fn set_kind(&self, kind: &VirtualItemKind) { - let label = match kind { + fn update(&self) { + let Some(kind) = self + .virtual_item + .obj() + .map(|virtual_item| virtual_item.kind()) + else { + return; + }; + + let label = match &kind { VirtualItemKind::TimelineStart => { gettext("This is the start of the visible history") } @@ -97,12 +138,4 @@ impl DividerRow { pub fn new() -> Self { glib::Object::new() } - - /// Set the kind of this divider. - /// - /// Panics if the kind is not `TimelineStart`, `DayDivider` or - /// `NewMessages`. - pub(crate) fn set_kind(&self, kind: &VirtualItemKind) { - self.imp().set_kind(kind); - } } diff --git a/src/session/view/content/room_history/divider_row.ui b/src/session/view/content/room_history/divider_row.ui index 56982936..286abeb9 100644 --- a/src/session/view/content/room_history/divider_row.ui +++ b/src/session/view/content/room_history/divider_row.ui @@ -4,6 +4,9 @@ inner_label + False 20 10 diff --git a/src/session/view/content/room_history/item_row.rs b/src/session/view/content/room_history/event_row.rs similarity index 78% rename from src/session/view/content/room_history/item_row.rs rename to src/session/view/content/room_history/event_row.rs index accffa6a..dda21fd6 100644 --- a/src/session/view/content/room_history/item_row.rs +++ b/src/session/view/content/room_history/event_row.rs @@ -5,16 +5,16 @@ use matrix_sdk_ui::timeline::{TimelineEventItemId, TimelineItemContent}; use ruma::events::room::message::MessageType; use tracing::error; -use super::{DividerRow, MessageRow, RoomHistory, StateRow, TypingRow}; +use super::{MessageRow, RoomHistory, StateRow}; use crate::{ components::ContextMenuBin, prelude::*, session::{ - model::{Event, MessageState, Room, TimelineItem, VirtualItem, VirtualItemKind}, + model::{Event, MessageState, Room}, view::{content::room_history::message_toolbar::ComposerState, EventDetailsDialog}, }, spawn, spawn_tokio, toast, - utils::BoundObjectWeakRef, + utils::{BoundObject, BoundObjectWeakRef}, }; mod imp { @@ -23,36 +23,34 @@ mod imp { use super::*; #[derive(Debug, Default, glib::Properties)] - #[properties(wrapper_type = super::ItemRow)] - pub struct ItemRow { + #[properties(wrapper_type = super::EventRow)] + pub struct EventRow { /// The ancestor room history of this row. #[property(get, set = Self::set_room_history, construct_only)] room_history: glib::WeakRef, message_toolbar_handler: RefCell>, composer_state: BoundObjectWeakRef, - /// The [`TimelineItem`] presented by this row. - #[property(get, set = Self::set_item, explicit_notify, nullable)] - item: RefCell>, - item_handlers: RefCell>, + /// The event presented by this row. + #[property(get, set = Self::set_event, explicit_notify, nullable)] + event: BoundObject, /// The event action group of this row. #[property(get, set = Self::set_action_group)] action_group: RefCell>, permissions_handler: RefCell>, - binding: RefCell>, } #[glib::object_subclass] - impl ObjectSubclass for ItemRow { - const NAME: &'static str = "RoomHistoryItemRow"; - type Type = super::ItemRow; + impl ObjectSubclass for EventRow { + const NAME: &'static str = "RoomHistoryEventRow"; + type Type = super::EventRow; type ParentType = ContextMenuBin; fn class_init(klass: &mut Self::Class) { - klass.set_css_name("room-history-row"); + klass.set_css_name("event-row"); klass.set_accessible_role(gtk::AccessibleRole::ListItem); klass.install_action( - "room-history-row.enable-copy-image", + "event-row.enable-copy-image", Some(&bool::static_variant_type()), |obj, _, param| { let enable = param @@ -79,17 +77,19 @@ mod imp { } #[glib::derived_properties] - impl ObjectImpl for ItemRow { + impl ObjectImpl for EventRow { fn constructed(&self) { self.parent_constructed(); + let obj = self.obj(); - self.obj().connect_parent_notify(|obj| { + obj.connect_parent_notify(|obj| { obj.imp().update_highlight(); }); + obj.add_css_class("room-history-row"); } fn dispose(&self) { - self.disconnect_item_signals(); + self.disconnect_event_signals(); if let Some(handler) = self.message_toolbar_handler.take() { if let Some(room_history) = self.room_history.upgrade() { @@ -99,16 +99,16 @@ mod imp { } } - impl WidgetImpl for ItemRow {} + impl WidgetImpl for EventRow {} - impl ContextMenuBinImpl for ItemRow { + impl ContextMenuBinImpl for EventRow { fn menu_opened(&self) { let Some(room_history) = self.room_history.upgrade() else { return; }; let obj = self.obj(); - let Some(event) = self.item.borrow().clone().and_downcast::() else { + let Some(event) = self.event.obj() else { obj.set_popover(None); return; }; @@ -118,7 +118,7 @@ mod imp { return; } - let menu = room_history.item_context_menu(); + let menu = room_history.event_context_menu(); // Reset the state when the popover is closed. let closed_handler_cell: Rc>> = @@ -154,7 +154,7 @@ mod imp { } } - impl ItemRow { + impl EventRow { /// Set the ancestor room history of this row. fn set_room_history(&self, room_history: &RoomHistory) { self.room_history.set(Some(room_history)); @@ -202,167 +202,82 @@ mod imp { ); } - /// Disconnect the signal handlers depending on the item. - fn disconnect_item_signals(&self) { - if let Some(item) = self.item.borrow().clone() { - for handler in self.item_handlers.borrow_mut().drain(..) { - item.disconnect(handler); - } + /// Disconnect the signal handlers. + fn disconnect_event_signals(&self) { + if let Some(event) = self.event.obj() { + self.event.disconnect_signals(); - if let Some(event) = item.downcast_ref::() { - if let Some(handler) = self.permissions_handler.take() { - event.room().permissions().disconnect(handler); - } + if let Some(handler) = self.permissions_handler.take() { + event.room().permissions().disconnect(handler); } } - - if let Some(binding) = self.binding.take() { - binding.unbind(); - } } - /// Set the [`TimelineItem`] presented by this row. - /// - /// This 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) { + /// Set the event presented by this row. + fn set_event(&self, event: Option) { // Reinitialize the header. self.obj().remove_css_class("has-header"); - self.disconnect_item_signals(); - - if let Some(item) = &item { - if let Some(event) = item.downcast_ref::() { - self.set_event(event); - } else if let Some(item) = item.downcast_ref::() { - self.set_virtual_item(item); - } - } - self.item.replace(item); - - self.update_highlight(); - } - - /// The event displayed by this row, if any. - fn event(&self) -> Option { - self.item.borrow().clone().and_downcast() - } - - /// Set the event to display. - fn set_event(&self, event: &Event) { - let state_notify_handler = event.connect_state_notify(clone!( - #[weak(rename_to = imp)] - self, - move |event| { - imp.update_event_actions(Some(event.upcast_ref())); - } - )); - - let source_notify_handler = event.connect_source_notify(clone!( - #[weak(rename_to = imp)] - self, - move |event| { - imp.build_event_widget(event.clone()); - imp.update_event_actions(Some(event.upcast_ref())); - } - )); + self.disconnect_event_signals(); - let edit_source_notify_handler = event.connect_latest_edit_source_notify(clone!( - #[weak(rename_to = imp)] - self, - move |event| { - imp.build_event_widget(event.clone()); - imp.update_event_actions(Some(event.upcast_ref())); - } - )); - - let is_highlighted_notify_handler = event.connect_is_highlighted_notify(clone!( - #[weak(rename_to = imp)] - self, - move |_| { - imp.update_highlight(); - } - )); - - self.item_handlers.borrow_mut().extend([ - state_notify_handler, - source_notify_handler, - edit_source_notify_handler, - is_highlighted_notify_handler, - ]); - - let permissions_handler = event.room().permissions().connect_changed(clone!( - #[weak(rename_to = imp)] - self, - #[weak] - event, - move |_| { - imp.update_event_actions(Some(event.upcast_ref())); - } - )); - self.permissions_handler.replace(Some(permissions_handler)); - - self.build_event_widget(event.clone()); - self.update_event_actions(Some(event.upcast_ref())); - } - - /// Set the virtual item to display. - fn set_virtual_item(&self, virtual_item: &VirtualItem) { - self.obj().set_popover(None); - self.update_event_actions(None); - - let kind_handler = virtual_item.connect_kind_changed(clone!( - #[weak(rename_to = imp)] - self, - move |virtual_item| { - imp.build_virtual_item(virtual_item); - } - )); - self.item_handlers.borrow_mut().push(kind_handler); - - self.build_virtual_item(virtual_item); - } - - /// Construct the widget for the given virtual item. - fn build_virtual_item(&self, virtual_item: &VirtualItem) { - let obj = self.obj(); - let kind = &virtual_item.kind(); + if let Some(event) = event { + let permissions_handler = event.room().permissions().connect_changed(clone!( + #[weak(rename_to = imp)] + self, + #[weak] + event, + move |_| { + imp.update_actions(&event); + } + )); + self.permissions_handler.replace(Some(permissions_handler)); - match kind { - VirtualItemKind::Spinner => { - if !obj - .child() - .is_some_and(|widget| widget.is::()) - { - obj.set_child(Some(&spinner())); + let state_notify_handler = event.connect_state_notify(clone!( + #[weak(rename_to = imp)] + self, + move |event| { + imp.update_actions(event); } - } - VirtualItemKind::Typing => { - let child = if let Some(child) = obj.child().and_downcast::() { - child - } else { - let child = TypingRow::new(); - obj.set_child(Some(&child)); - child - }; + )); + let source_notify_handler = event.connect_source_notify(clone!( + #[weak(rename_to = imp)] + self, + move |event| { + imp.build_event_widget(event.clone()); + imp.update_actions(event); + } + )); + let edit_source_notify_handler = event.connect_latest_edit_source_notify(clone!( + #[weak(rename_to = imp)] + self, + move |event| { + imp.build_event_widget(event.clone()); + imp.update_actions(event); + } + )); + let is_highlighted_notify_handler = event.connect_is_highlighted_notify(clone!( + #[weak(rename_to = imp)] + self, + move |_| { + imp.update_highlight(); + } + )); - let typing_list = virtual_item.room().typing_list(); - child.set_list(Some(typing_list)); - } - VirtualItemKind::TimelineStart - | VirtualItemKind::DayDivider(_) - | VirtualItemKind::NewMessages => { - let divider = if let Some(divider) = obj.child().and_downcast::() { - divider - } else { - let divider = DividerRow::new(); - obj.set_child(Some(÷r)); - divider - }; - divider.set_kind(kind); - } + self.event.set( + event.clone(), + vec![ + state_notify_handler, + source_notify_handler, + edit_source_notify_handler, + is_highlighted_notify_handler, + ], + ); + + self.update_actions(&event); + self.build_event_widget(event); } + + self.update_highlight(); } /// Set the event action group of this row. @@ -408,7 +323,7 @@ mod imp { fn update_highlight(&self) { let obj = self.obj(); - let highlight = self.event().is_some_and(|event| event.is_highlighted()); + let highlight = self.event.obj().is_some_and(|event| event.is_highlighted()); if highlight { obj.add_css_class("highlight"); } else { @@ -452,7 +367,8 @@ mod imp { let obj = self.obj(); if related_event_id.is_some_and(|identifier| { - self.event() + self.event + .obj() .is_some_and(|event| event.matches_identifier(identifier)) }) { obj.add_css_class("selected"); @@ -462,18 +378,9 @@ mod imp { } /// Update the actions available for the given event. - /// - /// Unsets the actions if `event` is `None`. - fn update_event_actions(&self, event: Option<&Event>) { + fn update_actions(&self, event: &Event) { let obj = self.obj(); - let Some(event) = event else { - obj.insert_action_group("event", None::<&gio::ActionGroup>); - self.set_action_group(None); - obj.set_has_context_menu(false); - return; - }; - let action_group = gio::SimpleActionGroup::new(); let room = event.room(); let has_event_id = event.event_id().is_some(); @@ -487,7 +394,7 @@ mod imp { obj, move |_, _, _| { spawn!(async move { - let Some(event) = obj.imp().event() else { + let Some(event) = obj.imp().event.obj() else { return; }; let Some(permalink) = event.matrix_to_uri().await else { @@ -506,7 +413,7 @@ mod imp { #[weak] obj, move |_, _, _| { - let Some(event) = obj.imp().event() else { + let Some(event) = obj.imp().event.obj() else { return; }; @@ -557,7 +464,7 @@ mod imp { } } - self.add_message_actions(&action_group, &room, event); + self.add_message_like_actions(&action_group, &room, event); obj.insert_action_group("event", Some(&action_group)); self.set_action_group(Some(action_group)); @@ -565,11 +472,11 @@ mod imp { } /// Add actions to the given action group for the given event, if it is - /// a message. + /// message-like. /// - /// See [`Event::is_message`] for the definition of a message-like + /// See [`Event::is_message_like()`] for the definition of a message /// event. - fn add_message_actions( + fn add_message_like_actions( &self, action_group: &gio::SimpleActionGroup, room: &Room, @@ -642,7 +549,7 @@ mod imp { #[weak(rename_to = imp)] self, move |_, _, _| { - let Some(event) = imp.event() else { + let Some(event) = imp.event.obj() else { error!("Could not reply to timeline item that is not an event"); return; }; @@ -666,13 +573,13 @@ mod imp { .build()]); } - self.add_message_content_actions(action_group, room, event); + self.add_message_actions(action_group, room, event); } /// Add actions to the given action group for the given event, if it - /// includes message content. + /// is a message. #[allow(clippy::too_many_lines)] - fn add_message_content_actions( + fn add_message_actions( &self, action_group: &gio::SimpleActionGroup, room: &Room, @@ -751,7 +658,7 @@ mod imp { .child() .and_downcast::() .and_then(|r| r.texture()) - .expect("An ItemRow with an image should have a texture"); + .expect("An EventRow with an image should have a texture"); obj.clipboard().set_texture(&texture); toast!(obj, gettext("Thumbnail copied to clipboard")); @@ -815,7 +722,7 @@ mod imp { /// Copy the text of this row. fn copy_text(&self) { - let Some(event) = self.event() else { + let Some(event) = self.event.obj() else { error!("Could not copy text of timeline item that is not an event"); return; }; @@ -851,12 +758,12 @@ mod imp { /// Edit the message of this row. fn edit_message(&self) { - let Some(event) = self.event() else { + let Some(event) = self.event.obj() else { error!("Could not edit timeline item that is not an event"); return; }; let Some(event_id) = event.event_id() else { - error!("Event to edit does not have an event ID"); + error!("Could not edit event without an event ID"); return; }; @@ -875,7 +782,7 @@ mod imp { #[weak(rename_to = imp)] self, async move { - let Some(event) = imp.event() else { + let Some(event) = imp.event.obj() else { error!("Could not save file of timeline item that is not an event"); return; }; @@ -896,7 +803,7 @@ mod imp { /// Redact the event of this row. async fn redact_message(&self) { - let Some(event) = self.event() else { + let Some(event) = self.event.obj() else { error!("Could not redact timeline item that is not an event"); return; }; @@ -930,7 +837,7 @@ mod imp { /// Toggle the reaction with the given key for the event of this row. async fn toggle_reaction(&self, key: String) { - let Some(event) = self.event() else { + let Some(event) = self.event.obj() else { error!("Could not toggle reaction on timeline item that is not an event"); return; }; @@ -942,7 +849,7 @@ mod imp { /// Report the current event. async fn report_event(&self) { - let Some(event) = self.event() else { + let Some(event) = self.event.obj() else { error!("Could not report timeline item that is not an event"); return; }; @@ -998,7 +905,7 @@ mod imp { /// Cancel sending the event of this row. async fn cancel_send(&self) { - let Some(event) = self.event() else { + let Some(event) = self.event.obj() else { error!("Could not discard timeline item that is not an event"); return; }; @@ -1017,25 +924,15 @@ mod imp { } glib::wrapper! { - /// A row presenting an item in the room history. - pub struct ItemRow(ObjectSubclass) + /// A row presenting an event in the room history. + pub struct EventRow(ObjectSubclass) @extends gtk::Widget, ContextMenuBin, @implements gtk::Accessible; } -impl ItemRow { +impl EventRow { pub fn new(room_history: &RoomHistory) -> Self { glib::Object::builder() .property("room-history", room_history) .build() } } - -/// Create a spinner widget. -fn spinner() -> adw::Spinner { - adw::Spinner::builder() - .margin_top(12) - .margin_bottom(12) - .height_request(24) - .width_request(24) - .build() -} diff --git a/src/session/view/content/room_history/item_row_context_menu.rs b/src/session/view/content/room_history/event_row_context_menu.rs similarity index 96% rename from src/session/view/content/room_history/item_row_context_menu.rs rename to src/session/view/content/room_history/event_row_context_menu.rs index 428f86d5..aa0eefe3 100644 --- a/src/session/view/content/room_history/item_row_context_menu.rs +++ b/src/session/view/content/room_history/event_row_context_menu.rs @@ -8,9 +8,11 @@ use gtk::{ use crate::{session::model::ReactionList, utils::BoundObject}; -/// Helper struct for the context menu of an `ItemRow`. +/// Helper struct for the context menu of an [`EventRow`]. +/// +/// [`EventRow`]: super::EventRow #[derive(Debug)] -pub(super) struct ItemRowContextMenu { +pub(super) struct EventRowContextMenu { /// The popover of the context menu. pub(super) popover: gtk::PopoverMenu, /// The menu model of the popover. @@ -19,7 +21,7 @@ pub(super) struct ItemRowContextMenu { quick_reaction_chooser: QuickReactionChooser, } -impl ItemRowContextMenu { +impl EventRowContextMenu { /// The identifier in the context menu for the quick reaction chooser. const QUICK_REACTION_CHOOSER_ID: &str = "quick-reaction-chooser"; @@ -29,7 +31,7 @@ impl ItemRowContextMenu { .menu_model .item_link(0, gio::MENU_LINK_SECTION) .and_downcast::() - .expect("item row context menu has at least one section"); + .expect("event row context menu should have at least one section"); first_section .item_attribute_value(0, "custom", Some(&String::static_variant_type())) .and_then(|variant| variant.get::()) @@ -69,13 +71,13 @@ impl ItemRowContextMenu { } } -impl Default for ItemRowContextMenu { +impl Default for EventRowContextMenu { fn default() -> Self { let menu_model = gtk::Builder::from_resource( - "/org/gnome/Fractal/ui/session/view/content/room_history/event_context_menu.ui", + "/org/gnome/Fractal/ui/session/view/content/room_history/event_row_context_menu.ui", ) .object::("event-menu") - .expect("resource and menu exist"); + .expect("GResource and menu should exist"); let popover = gtk::PopoverMenu::builder() .has_arrow(false) diff --git a/src/session/view/content/room_history/event_context_menu.ui b/src/session/view/content/room_history/event_row_context_menu.ui similarity index 100% rename from src/session/view/content/room_history/event_context_menu.ui rename to src/session/view/content/room_history/event_row_context_menu.ui diff --git a/src/session/view/content/room_history/message_row/visual_media.rs b/src/session/view/content/room_history/message_row/visual_media.rs index 4a8fa07c..c28c8154 100644 --- a/src/session/view/content/room_history/message_row/visual_media.rs +++ b/src/session/view/content/room_history/message_row/visual_media.rs @@ -620,13 +620,10 @@ mod imp { if self .obj() - .activate_action( - "room-history-row.enable-copy-image", - Some(&enable.to_variant()), - ) + .activate_action("event-row.enable-copy-image", Some(&enable.to_variant())) .is_err() { - error!("Could not change state of copy-image action: `room-history-row.enable-copy-image` action not found"); + error!("Could not change state of copy-image action: `event-row.enable-copy-image` action not found"); } } diff --git a/src/session/view/content/room_history/message_toolbar/composer_state.rs b/src/session/view/content/room_history/message_toolbar/composer_state.rs index 8c748e13..f2739607 100644 --- a/src/session/view/content/room_history/message_toolbar/composer_state.rs +++ b/src/session/view/content/room_history/message_toolbar/composer_state.rs @@ -671,7 +671,7 @@ impl MessageEventSource { /// /// Returns `None` if the event is not a message. pub(crate) fn from_event(event: Event) -> Option { - (event.event_id().is_some() && event.is_message()).then_some(Self::Event(event)) + (event.can_be_replied_to()).then_some(Self::Event(event)) } /// The ID of the underlying event. diff --git a/src/session/view/content/room_history/mod.rs b/src/session/view/content/room_history/mod.rs index 25855bf5..6683afd4 100644 --- a/src/session/view/content/room_history/mod.rs +++ b/src/session/view/content/room_history/mod.rs @@ -9,8 +9,8 @@ use ruma::{api::client::receipt::create_receipt::v3::ReceiptType, OwnedEventId}; use tracing::{error, warn}; mod divider_row; -mod item_row; -mod item_row_context_menu; +mod event_row; +mod event_row_context_menu; mod member_timestamp; mod message_row; mod message_toolbar; @@ -22,7 +22,7 @@ mod typing_row; mod verification_info_bar; use self::{ - divider_row::DividerRow, item_row::ItemRow, item_row_context_menu::ItemRowContextMenu, + divider_row::DividerRow, event_row::EventRow, event_row_context_menu::EventRowContextMenu, message_row::MessageRow, message_toolbar::MessageToolbar, read_receipts_list::ReadReceiptsList, sender_avatar::SenderAvatar, state_row::StateRow, title::RoomHistoryTitle, typing_row::TypingRow, verification_info_bar::VerificationInfoBar, @@ -33,6 +33,7 @@ use crate::{ prelude::*, session::model::{ Event, MemberList, Membership, ReceiptPosition, Room, TargetRoomCategory, Timeline, + VirtualItem, VirtualItemKind, }, spawn, toast, utils::{BoundObject, LoadingState, TemplateCallbacks}, @@ -88,7 +89,7 @@ mod imp { tombstoned_banner: TemplateChild, #[template_child] drag_overlay: TemplateChild, - item_context_menu: OnceCell, + event_context_menu: OnceCell, sender_context_menu: OnceCell, /// The timeline currently displayed. #[property(get, set = Self::set_timeline, explicit_notify, nullable)] @@ -124,7 +125,6 @@ mod imp { type ParentType = adw::Bin; fn class_init(klass: &mut Self::Class) { - ItemRow::ensure_type(); VerificationInfoBar::ensure_type(); Self::bind_template(klass); @@ -257,19 +257,45 @@ mod imp { let obj = self.obj(); let factory = gtk::SignalListItemFactory::new(); - factory.connect_setup(clone!( + factory.connect_setup(move |_, list_item| { + let Some(list_item) = list_item.downcast_ref::() else { + error!("List item factory did not receive a list item: {list_item:?}"); + return; + }; + + list_item.set_activatable(false); + list_item.set_selectable(false); + }); + factory.connect_bind(clone!( #[weak] obj, - move |_, item| { - let Some(item) = item.downcast_ref::() else { - error!("List item factory did not receive a list item: {item:?}"); + move |_, list_item| { + let Some(list_item) = list_item.downcast_ref::() else { + error!("List item factory did not receive a list item: {list_item:?}"); + return; + }; + let Some(item) = list_item.item() else { + list_item.set_child(None::<>k::Widget>); return; }; - let row = ItemRow::new(&obj); - item.set_child(Some(&row)); - item.bind_property("item", &row, "item").build(); - item.set_activatable(false); - item.set_selectable(false); + + if let Some(event) = item.downcast_ref::() { + let child = + if let Some(child) = list_item.child().and_downcast::() { + child + } else { + let child = EventRow::new(&obj); + list_item.set_child(Some(&child)); + child + }; + child.set_event(Some(event.clone())); + } else if let Some(virtual_item) = item.downcast_ref::() { + set_virtual_item_child(list_item, virtual_item); + } else { + error!( + "Could not build widget for unsupported room history item: {item:?}" + ); + } } )); self.listview.set_factory(Some(&factory)); @@ -840,9 +866,8 @@ mod imp { if top_in_view || bottom_in_view || content_in_view { if let Some(event_id) = item .first_child() - .and_downcast::() - .and_then(|row| row.item()) - .and_downcast::() + .and_downcast::() + .and_then(|row| row.event()) .and_then(|event| event.event_id()) { return Some(event_id); @@ -1004,9 +1029,9 @@ mod imp { } } - /// The context menu for the item rows. - pub(super) fn item_context_menu(&self) -> &ItemRowContextMenu { - self.item_context_menu.get_or_init(Default::default) + /// The context menu for the [`EventRow`]s. + pub(super) fn event_context_menu(&self) -> &EventRowContextMenu { + self.event_context_menu.get_or_init(Default::default) } /// The context menu for the sender avatars. @@ -1079,9 +1104,9 @@ impl RoomHistory { self.imp().message_toolbar.handle_paste_action(); } - /// The context menu for the item rows. - fn item_context_menu(&self) -> &ItemRowContextMenu { - self.imp().item_context_menu() + /// The context menu for the [`EventRow`]s. + fn event_context_menu(&self) -> &EventRowContextMenu { + self.imp().event_context_menu() } /// The context menu for the sender avatars. @@ -1089,3 +1114,54 @@ impl RoomHistory { self.imp().sender_context_menu() } } + +/// Set the proper child of the given `GtkListItem` for the given +/// [`VirtualItem`]. +/// +/// Constructs or reuses the child widget as necessary. +fn set_virtual_item_child(list_item: >k::ListItem, virtual_item: &VirtualItem) { + let kind = &virtual_item.kind(); + + match kind { + VirtualItemKind::Spinner => { + if !list_item + .child() + .is_some_and(|widget| widget.is::()) + { + let spinner = adw::Spinner::builder() + .margin_top(12) + .margin_bottom(12) + .height_request(24) + .width_request(24) + .build(); + spinner.add_css_class("room-history-row"); + spinner.set_accessible_role(gtk::AccessibleRole::ListItem); + list_item.set_child(Some(&spinner)); + } + } + VirtualItemKind::Typing => { + let child = if let Some(child) = list_item.child().and_downcast::() { + child + } else { + let child = TypingRow::new(); + list_item.set_child(Some(&child)); + child + }; + + let typing_list = virtual_item.room().typing_list(); + child.set_list(Some(typing_list)); + } + VirtualItemKind::TimelineStart + | VirtualItemKind::DayDivider(_) + | VirtualItemKind::NewMessages => { + let divider = if let Some(divider) = list_item.child().and_downcast::() { + divider + } else { + let divider = DividerRow::new(); + list_item.set_child(Some(÷r)); + divider + }; + divider.set_virtual_item(Some(virtual_item)); + } + } +} diff --git a/src/session/view/content/room_history/typing_row.rs b/src/session/view/content/room_history/typing_row.rs index df0c2fa1..84674d1c 100644 --- a/src/session/view/content/room_history/typing_row.rs +++ b/src/session/view/content/room_history/typing_row.rs @@ -42,7 +42,7 @@ mod imp { Self::bind_template(klass); klass.set_css_name("typing-row"); - klass.set_accessible_role(gtk::AccessibleRole::Status); + klass.set_accessible_role(gtk::AccessibleRole::ListItem); } fn instance_init(obj: &InitializingObject) { diff --git a/src/session/view/content/room_history/typing_row.ui b/src/session/view/content/room_history/typing_row.ui index 58509428..586fb732 100644 --- a/src/session/view/content/room_history/typing_row.ui +++ b/src/session/view/content/room_history/typing_row.ui @@ -4,6 +4,9 @@ label + slide-up diff --git a/src/ui-resources.gresource.xml b/src/ui-resources.gresource.xml index 3ef9018b..4cffc795 100644 --- a/src/ui-resources.gresource.xml +++ b/src/ui-resources.gresource.xml @@ -107,7 +107,7 @@ session/view/content/room_details/permissions/permissions_subpage.ui session/view/content/room_details/permissions/select_member_row.ui session/view/content/room_history/divider_row.ui - session/view/content/room_history/event_context_menu.ui + session/view/content/room_history/event_row_context_menu.ui session/view/content/room_history/member_timestamp/row.ui session/view/content/room_history/message_row/audio.ui session/view/content/room_history/message_row/file.ui