Browse Source

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.
merge-requests/1958/merge
Kévin Commaille 11 months ago
parent
commit
4372f80a6b
No known key found for this signature in database
GPG Key ID: C971D9DBC9D678D
  1. 10
      data/resources/stylesheet/_room_history.scss
  2. 6
      po/POTFILES.in
  3. 24
      src/session/model/room/timeline/event/mod.rs
  4. 63
      src/session/view/content/room_history/divider_row.rs
  5. 3
      src/session/view/content/room_history/divider_row.ui
  6. 327
      src/session/view/content/room_history/event_row.rs
  7. 16
      src/session/view/content/room_history/event_row_context_menu.rs
  8. 0
      src/session/view/content/room_history/event_row_context_menu.ui
  9. 7
      src/session/view/content/room_history/message_row/visual_media.rs
  10. 2
      src/session/view/content/room_history/message_toolbar/composer_state.rs
  11. 122
      src/session/view/content/room_history/mod.rs
  12. 2
      src/session/view/content/room_history/typing_row.rs
  13. 3
      src/session/view/content/room_history/typing_row.ui
  14. 2
      src/ui-resources.gresource.xml

10
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 {

6
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

24
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;
}

63
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<gtk::Label>,
/// The virtual item presented by this row.
#[property(get, set = Self::set_virtual_item, explicit_notify, nullable)]
virtual_item: BoundObject<VirtualItem>,
}
#[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<Self>) {
@ -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<VirtualItem>) {
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);
}
}

3
src/session/view/content/room_history/divider_row.ui

@ -4,6 +4,9 @@
<accessibility>
<relation name="labelled-by">inner_label</relation>
</accessibility>
<style>
<class name="room-history-row"/>
</style>
<property name="can-focus">False</property>
<property name="margin-top">20</property>
<property name="margin-bottom">10</property>

327
src/session/view/content/room_history/item_row.rs → 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<RoomHistory>,
message_toolbar_handler: RefCell<Option<glib::SignalHandlerId>>,
composer_state: BoundObjectWeakRef<ComposerState>,
/// The [`TimelineItem`] presented by this row.
#[property(get, set = Self::set_item, explicit_notify, nullable)]
item: RefCell<Option<TimelineItem>>,
item_handlers: RefCell<Vec<glib::SignalHandlerId>>,
/// The event presented by this row.
#[property(get, set = Self::set_event, explicit_notify, nullable)]
event: BoundObject<Event>,
/// The event action group of this row.
#[property(get, set = Self::set_action_group)]
action_group: RefCell<Option<gio::SimpleActionGroup>>,
permissions_handler: RefCell<Option<glib::SignalHandlerId>>,
binding: RefCell<Option<glib::Binding>>,
}
#[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::<Event>() 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<RefCell<Option<glib::signal::SignalHandlerId>>> =
@ -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::<Event>() {
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<TimelineItem>) {
/// Set the event presented by this row.
fn set_event(&self, event: Option<Event>) {
// 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::<Event>() {
self.set_event(event);
} else if let Some(item) = item.downcast_ref::<VirtualItem>() {
self.set_virtual_item(item);
}
}
self.item.replace(item);
self.update_highlight();
}
/// The event displayed by this row, if any.
fn event(&self) -> Option<Event> {
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::<adw::Spinner>())
{
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::<TypingRow>() {
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::<DividerRow>() {
divider
} else {
let divider = DividerRow::new();
obj.set_child(Some(&divider));
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::<MessageRow>()
.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<imp::ItemRow>)
/// A row presenting an event in the room history.
pub struct EventRow(ObjectSubclass<imp::EventRow>)
@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()
}

16
src/session/view/content/room_history/item_row_context_menu.rs → 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::<gio::Menu>()
.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::<String>())
@ -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::<gio::Menu>("event-menu")
.expect("resource and menu exist");
.expect("GResource and menu should exist");
let popover = gtk::PopoverMenu::builder()
.has_arrow(false)

0
src/session/view/content/room_history/event_context_menu.ui → src/session/view/content/room_history/event_row_context_menu.ui

7
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");
}
}

2
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<Self> {
(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.

122
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<adw::Banner>,
#[template_child]
drag_overlay: TemplateChild<DragOverlay>,
item_context_menu: OnceCell<ItemRowContextMenu>,
event_context_menu: OnceCell<EventRowContextMenu>,
sender_context_menu: OnceCell<gtk::PopoverMenu>,
/// 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::<gtk::ListItem>() 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::<gtk::ListItem>() else {
error!("List item factory did not receive a list item: {item:?}");
move |_, list_item| {
let Some(list_item) = list_item.downcast_ref::<gtk::ListItem>() 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::<&gtk::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::<Event>() {
let child =
if let Some(child) = list_item.child().and_downcast::<EventRow>() {
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::<VirtualItem>() {
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::<ItemRow>()
.and_then(|row| row.item())
.and_downcast::<Event>()
.and_downcast::<EventRow>()
.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: &gtk::ListItem, virtual_item: &VirtualItem) {
let kind = &virtual_item.kind();
match kind {
VirtualItemKind::Spinner => {
if !list_item
.child()
.is_some_and(|widget| widget.is::<adw::Spinner>())
{
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::<TypingRow>() {
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::<DividerRow>() {
divider
} else {
let divider = DividerRow::new();
list_item.set_child(Some(&divider));
divider
};
divider.set_virtual_item(Some(virtual_item));
}
}
}

2
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<Self>) {

3
src/session/view/content/room_history/typing_row.ui

@ -4,6 +4,9 @@
<accessibility>
<relation name="labelled-by">label</relation>
</accessibility>
<style>
<class name="room-history-row"/>
</style>
<child>
<object class="GtkRevealer">
<property name="transition-type">slide-up</property>

2
src/ui-resources.gresource.xml

@ -107,7 +107,7 @@
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_details/permissions/permissions_subpage.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_details/permissions/select_member_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/divider_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/event_context_menu.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/event_row_context_menu.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/member_timestamp/row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_row/audio.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_row/file.ui</file>

Loading…
Cancel
Save