From 118f4ca1b093d11056993275e209409e00c0b605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 19 Dec 2023 19:21:07 +0100 Subject: [PATCH] room-history: Port to glib::Properties macro --- src/session/model/room/mod.rs | 9 + .../view/content/room_history/divider_row.rs | 61 ++-- .../view/content/room_history/divider_row.ui | 2 +- .../view/content/room_history/item_row.rs | 341 ++++++++---------- .../room_history/member_timestamp/mod.rs | 79 +--- .../room_history/member_timestamp/row.rs | 66 +--- .../content/room_history/message_row/audio.rs | 66 +--- .../room_history/message_row/content.rs | 62 +--- .../content/room_history/message_row/file.rs | 107 ++---- .../content/room_history/message_row/media.rs | 133 ++----- .../message_row/message_state_stack.rs | 186 ++++------ .../content/room_history/message_row/mod.rs | 231 +++++------- .../room_history/message_row/reaction/mod.rs | 154 +++----- .../message_row/reaction/reaction_popover.rs | 69 +--- .../room_history/message_row/reaction_list.rs | 2 - .../content/room_history/message_row/reply.rs | 3 +- .../content/room_history/message_row/text.rs | 62 +--- .../message_toolbar/attachment_dialog.rs | 1 + .../completion/completion_popover.rs | 135 +++---- .../completion/completion_row.rs | 81 ++--- .../room_history/message_toolbar/mod.rs | 163 +++------ src/session/view/content/room_history/mod.rs | 332 +++++++---------- src/session/view/content/room_history/mod.ui | 2 +- .../room_history/read_receipts_list/mod.rs | 101 ++---- .../read_receipts_popover.rs | 62 +--- .../room_history/state_row/creation.rs | 1 + .../content/room_history/state_row/mod.rs | 82 ++--- .../room_history/state_row/tombstone.rs | 88 ++--- .../view/content/room_history/typing_row.rs | 155 ++++---- .../room_history/verification_info_bar.rs | 147 ++++---- 30 files changed, 1043 insertions(+), 1940 deletions(-) diff --git a/src/session/model/room/mod.rs b/src/session/model/room/mod.rs index 3838f35c..1ecd10d9 100644 --- a/src/session/model/room/mod.rs +++ b/src/session/model/room/mod.rs @@ -125,6 +125,10 @@ mod imp { pub predecessor_id: OnceCell, /// The ID of the successor of this Room, if this room was upgraded. pub successor_id: OnceCell, + /// The ID of the successor of this Room, if this room was upgraded, as + /// a string. + #[property(get = Self::successor_id_string)] + pub successor_id_string: PhantomData>, /// The successor of this Room, if this room was upgraded and the /// successor was joined. #[property(get)] @@ -275,6 +279,11 @@ mod imp { self.matrix_room.borrow().as_ref().unwrap().is_tombstoned() } + /// The ID of the successor of this Room, if this room was upgraded. + fn successor_id_string(&self) -> Option { + self.successor_id.get().map(ToString::to_string) + } + /// Set the notifications setting for this room. fn set_notifications_setting(&self, setting: NotificationsRoomSetting) { if self.notifications_setting.get() == setting { diff --git a/src/session/view/content/room_history/divider_row.rs b/src/session/view/content/room_history/divider_row.rs index d9e515cc..1522e1a6 100644 --- a/src/session/view/content/room_history/divider_row.rs +++ b/src/session/view/content/room_history/divider_row.rs @@ -2,15 +2,21 @@ use adw::subclass::prelude::*; use gtk::{glib, prelude::*, CompositeTemplate}; mod imp { + use std::marker::PhantomData; + 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] - pub label: TemplateChild, + pub inner_label: TemplateChild, + /// The label of this divider. + #[property(get = Self::label, set = Self::set_label)] + label: PhantomData, } #[glib::object_subclass] @@ -28,37 +34,27 @@ mod imp { } } - impl ObjectImpl for DividerRow { - fn properties() -> &'static [glib::ParamSpec] { - use once_cell::sync::Lazy; - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![glib::ParamSpecString::builder("label") - .explicit_notify() - .build()] - }); + #[glib::derived_properties] + impl ObjectImpl for DividerRow {} - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - match pspec.name() { - "label" => self.obj().set_label(value.get().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - match pspec.name() { - "label" => self.obj().label().to_value(), - _ => unimplemented!(), - } - } - } impl WidgetImpl for DividerRow {} impl BinImpl for DividerRow {} + + impl DividerRow { + /// The label of this divider. + fn label(&self) -> String { + self.inner_label.text().into() + } + + /// Set the label of this divider. + fn set_label(&self, label: String) { + self.inner_label.set_text(&label); + } + } } glib::wrapper! { + /// A row presenting a divider in the timeline. pub struct DividerRow(ObjectSubclass) @extends gtk::Widget, adw::Bin, @implements gtk::Accessible; } @@ -71,15 +67,4 @@ impl DividerRow { pub fn with_label(label: String) -> Self { glib::Object::builder().property("label", &label).build() } - - /// The label of this divider. - pub fn set_label(&self, label: &str) { - self.imp().label.set_text(label); - self.notify("label"); - } - - /// Set the label of this divider. - pub fn label(&self) -> String { - self.imp().label.text().as_str().to_owned() - } } diff --git a/src/session/view/content/room_history/divider_row.ui b/src/session/view/content/room_history/divider_row.ui index 540b7e3c..449d3851 100644 --- a/src/session/view/content/room_history/divider_row.ui +++ b/src/session/view/content/room_history/divider_row.ui @@ -17,7 +17,7 @@ - + diff --git a/src/session/view/content/room_history/item_row.rs b/src/session/view/content/room_history/item_row.rs index af531d4d..eabe7a41 100644 --- a/src/session/view/content/room_history/item_row.rs +++ b/src/session/view/content/room_history/item_row.rs @@ -23,10 +23,15 @@ mod imp { use super::*; - #[derive(Debug, Default)] + #[derive(Debug, Default, glib::Properties)] + #[properties(wrapper_type = super::ItemRow)] pub struct ItemRow { + /// The ancestor room history of this row. + #[property(get, set = Self::set_room_history, construct_only)] pub room_history: glib::WeakRef, pub message_toolbar_handler: RefCell>, + /// The [`TimelineItem`] presented by this row. + #[property(get, set = Self::set_item, explicit_notify, nullable)] pub item: RefCell>, pub action_group: RefCell>, pub notify_handlers: RefCell>, @@ -47,40 +52,8 @@ mod imp { } } + #[glib::derived_properties] impl ObjectImpl for ItemRow { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecObject::builder::("item").build(), - glib::ParamSpecObject::builder::("room-history") - .construct_only() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - - match pspec.name() { - "item" => obj.set_item(value.get().unwrap()), - "room-history" => obj.set_room_history(value.get().ok().as_ref()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "item" => obj.item().to_value(), - "room-history" => obj.room_history().to_value(), - _ => unimplemented!(), - } - } - fn constructed(&self) { self.parent_constructed(); @@ -96,7 +69,9 @@ mod imp { for handler in handlers { event.disconnect(handler); } - } else if let Some(binding) = self.binding.take() { + } + + if let Some(binding) = self.binding.take() { binding.unbind(); } @@ -128,7 +103,9 @@ mod imp { return; }; - let room_history = obj.room_history(); + let Some(room_history) = obj.room_history() else { + return; + }; let popover = room_history.item_context_menu().to_owned(); room_history.set_sticky(false); @@ -178,9 +155,148 @@ mod imp { obj.set_popover(Some(popover)); } } + + impl ItemRow { + /// Set the ancestor room history of this row. + fn set_room_history(&self, room_history: RoomHistory) { + let obj = self.obj(); + + self.room_history.set(Some(&room_history)); + + let related_event_handler = room_history + .message_toolbar() + .connect_related_event_notify(clone!(@weak obj => move |message_toolbar| { + obj.update_for_related_event(message_toolbar.related_event()); + })); + self.message_toolbar_handler + .replace(Some(related_event_handler)); + } + + /// 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) { + let obj = self.obj(); + + // Reinitialize the header. + obj.remove_css_class("has-header"); + + if let Some(event) = self.item.borrow().and_downcast_ref::() { + for handler in self.notify_handlers.take() { + event.disconnect(handler); + } + } + if let Some(binding) = self.binding.take() { + binding.unbind() + } + + if let Some(item) = &item { + if let Some(event) = item.downcast_ref::() { + let source_notify_handler = + event.connect_source_notify(clone!(@weak obj => move |event| { + obj.set_event_widget(event.clone()); + obj.set_action_group(obj.set_event_actions(Some(event.upcast_ref()))); + })); + let is_highlighted_notify_handler = + event.connect_is_highlighted_notify(clone!(@weak obj => move |_| { + obj.update_highlight(); + })); + self.notify_handlers + .replace(vec![source_notify_handler, is_highlighted_notify_handler]); + + obj.set_event_widget(event.clone()); + obj.set_action_group(obj.set_event_actions(Some(event.upcast_ref()))); + } else if let Some(item) = item.downcast_ref::() { + obj.set_popover(None); + obj.set_action_group(None); + obj.set_event_actions(None); + + match &*item.kind() { + VirtualItemKind::Spinner => { + if !obj.child().is_some_and(|widget| widget.is::()) { + let spinner = Spinner::default(); + spinner.set_margin_top(12); + spinner.set_margin_bottom(12); + obj.set_child(Some(&spinner)); + } + } + VirtualItemKind::Typing => { + let child = if let Some(child) = obj.child().and_downcast::() + { + child + } else { + let child = TypingRow::new(); + obj.set_child(Some(&child)); + child + }; + + child.set_list( + obj.room_history() + .and_then(|h| h.room()) + .map(|room| room.typing_list()), + ); + } + VirtualItemKind::TimelineStart => { + let label = gettext("This is the start of the visible history"); + + if let Some(child) = obj.child().and_downcast::() { + child.set_label(label); + } else { + let child = DividerRow::with_label(label); + obj.set_child(Some(&child)); + }; + } + VirtualItemKind::DayDivider(date) => { + let child = + if let Some(child) = obj.child().and_downcast::() { + child + } else { + let child = DividerRow::new(); + obj.set_child(Some(&child)); + child + }; + + let fmt = if date.year() == glib::DateTime::now_local().unwrap().year() + { + // Translators: This is a date format in the day divider without the + // year. For example, "Friday, May 5". + // Please use `-` before specifiers that add spaces on single + // digits. See `man strftime` or the documentation of g_date_time_format for the available specifiers: + gettext("%A, %B %-e") + } else { + // Translators: This is a date format in the day divider with the + // year. For ex. "Friday, May 5, + // 2023". Please use `-` before + // specifiers that add spaces on single digits. See `man strftime` or the documentation of g_date_time_format for the available specifiers: + gettext("%A, %B %-e, %Y") + }; + + child.set_label(date.format(&fmt).unwrap()) + } + VirtualItemKind::NewMessages => { + let label = gettext("New Messages"); + + if let Some(child) = obj.child().and_downcast::() { + child.set_label(label); + } else { + let child = DividerRow::with_label(label); + obj.set_child(Some(&child)); + }; + } + } + } + } + self.item.replace(item); + + obj.update_highlight(); + } + } } glib::wrapper! { + /// A row presenting an item in the room history. pub struct ItemRow(ObjectSubclass) @extends gtk::Widget, adw::Bin, ContextMenuBin, @implements gtk::Accessible; } @@ -193,31 +309,6 @@ impl ItemRow { .build() } - /// The ancestor room history of this row. - pub fn room_history(&self) -> RoomHistory { - self.imp().room_history.upgrade().unwrap() - } - - /// Set the ancestor room history of this row. - fn set_room_history(&self, room_history: Option<&RoomHistory>) { - let Some(room_history) = room_history else { - // Ignore missing `RoomHistory`. - return; - }; - - let imp = self.imp(); - imp.room_history.set(Some(room_history)); - - let related_event_handler = room_history.message_toolbar().connect_notify_local( - Some("related-event"), - clone!(@weak self as obj => move |message_toolbar, _| { - obj.update_for_related_event(message_toolbar.related_event()); - }), - ); - imp.message_toolbar_handler - .replace(Some(related_event_handler)); - } - pub fn action_group(&self) -> Option { self.imp().action_group.borrow().clone() } @@ -230,134 +321,6 @@ impl ItemRow { self.imp().action_group.replace(action_group); } - /// Get the row's [`TimelineItem`]. - pub fn item(&self) -> Option { - self.imp().item.borrow().clone() - } - - /// 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) { - let imp = self.imp(); - - // Reinitialize the header. - self.remove_css_class("has-header"); - - if let Some(event) = imp.item.borrow().and_downcast_ref::() { - let handlers = imp.notify_handlers.take(); - - for handler in handlers { - event.disconnect(handler); - } - } else if let Some(binding) = imp.binding.take() { - binding.unbind() - } - - if let Some(ref item) = item { - if let Some(event) = item.downcast_ref::() { - let source_notify_handler = - event.connect_source_notify(clone!(@weak self as obj => move |event| { - obj.set_event_widget(event.clone()); - obj.set_action_group(obj.set_event_actions(Some(event.upcast_ref()))); - })); - let is_highlighted_notify_handler = event.connect_notify_local( - Some("is-highlighted"), - clone!(@weak self as obj => move |_, _| { - obj.update_highlight(); - }), - ); - imp.notify_handlers - .replace(vec![source_notify_handler, is_highlighted_notify_handler]); - - self.set_event_widget(event.clone()); - self.set_action_group(self.set_event_actions(Some(event.upcast_ref()))); - } else if let Some(item) = item.downcast_ref::() { - self.set_popover(None); - self.set_action_group(None); - self.set_event_actions(None); - - match &*item.kind() { - VirtualItemKind::Spinner => { - if !self.child().map_or(false, |widget| widget.is::()) { - let spinner = Spinner::default(); - spinner.set_margin_top(12); - spinner.set_margin_bottom(12); - self.set_child(Some(&spinner)); - } - } - VirtualItemKind::Typing => { - let child = if let Some(child) = self.child().and_downcast::() { - child - } else { - let child = TypingRow::new(); - self.set_child(Some(&child)); - child - }; - - child.set_list( - self.room_history() - .room() - .as_ref() - .map(|room| room.typing_list()) - .as_ref(), - ); - } - VirtualItemKind::TimelineStart => { - let label = gettext("This is the start of the visible history"); - - if let Some(child) = self.child().and_downcast::() { - child.set_label(&label); - } else { - let child = DividerRow::with_label(label); - self.set_child(Some(&child)); - }; - } - VirtualItemKind::DayDivider(date) => { - let child = if let Some(child) = self.child().and_downcast::() { - child - } else { - let child = DividerRow::new(); - self.set_child(Some(&child)); - child - }; - - let fmt = if date.year() == glib::DateTime::now_local().unwrap().year() { - // Translators: This is a date format in the day divider without the - // year. For example, "Friday, May 5". - // Please use `-` before specifiers that add spaces on single digits. - // See `man strftime` or the documentation of g_date_time_format for the available specifiers: - gettext("%A, %B %-e") - } else { - // Translators: This is a date format in the day divider with the year. - // For ex. "Friday, May 5, 2023". - // Please use `-` before specifiers that add spaces on single digits. - // See `man strftime` or the documentation of g_date_time_format for the available specifiers: - gettext("%A, %B %-e, %Y") - }; - - child.set_label(&date.format(&fmt).unwrap()) - } - VirtualItemKind::NewMessages => { - let label = gettext("New Messages"); - - if let Some(child) = self.child().and_downcast::() { - child.set_label(&label); - } else { - let child = DividerRow::with_label(label); - self.set_child(Some(&child)); - }; - } - } - } - } - imp.item.replace(item); - - self.update_highlight(); - } - fn set_event_widget(&self, event: Event) { match event.content() { TimelineItemContent::MembershipChange(_) diff --git a/src/session/view/content/room_history/member_timestamp/mod.rs b/src/session/view/content/room_history/member_timestamp/mod.rs index 799688b7..62d10af4 100644 --- a/src/session/view/content/room_history/member_timestamp/mod.rs +++ b/src/session/view/content/room_history/member_timestamp/mod.rs @@ -8,17 +8,18 @@ use crate::session::model::Member; mod imp { use std::cell::Cell; - use once_cell::sync::Lazy; - use super::*; - #[derive(Debug, Default)] + #[derive(Debug, Default, glib::Properties)] + #[properties(wrapper_type = super::MemberTimestamp)] pub struct MemberTimestamp { /// The room member. + #[property(get, construct_only)] pub member: glib::WeakRef, /// The timestamp, in seconds since Unix Epoch. /// /// A value of 0 means no timestamp. + #[property(get, construct_only)] pub timestamp: Cell, } @@ -28,42 +29,8 @@ mod imp { type Type = super::MemberTimestamp; } - impl ObjectImpl for MemberTimestamp { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecObject::builder::("member") - .construct_only() - .build(), - glib::ParamSpecUInt64::builder("timestamp") - .construct_only() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "member" => obj.member().to_value(), - "timestamp" => obj.timestamp().to_value(), - _ => unimplemented!(), - } - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - - match pspec.name() { - "member" => obj.set_member(value.get::>().unwrap().as_ref()), - "timestamp" => obj.set_timestamp(value.get().unwrap()), - _ => unimplemented!(), - } - } - } + #[glib::derived_properties] + impl ObjectImpl for MemberTimestamp {} } glib::wrapper! { @@ -80,38 +47,4 @@ impl MemberTimestamp { .property("timestamp", timestamp.unwrap_or_default()) .build() } - - /// The room member of this read receipt. - pub fn member(&self) -> Option { - self.imp().member.upgrade() - } - - /// Set the room member of this read receipt. - fn set_member(&self, member: Option<&Member>) { - let Some(member) = member else { - // Ignore if there is no member. - return; - }; - - self.imp().member.set(Some(member)); - self.notify("member"); - } - - /// The timestamp of this read receipt, in seconds since Unix Epoch, if - /// any. - /// - /// A value of 0 means no timestamp. - pub fn timestamp(&self) -> u64 { - self.imp().timestamp.get() - } - - /// Set the timestamp of this read receipt. - pub fn set_timestamp(&self, ts: u64) { - if self.timestamp() == ts { - return; - } - - self.imp().timestamp.set(ts); - self.notify("timestamp"); - } } diff --git a/src/session/view/content/room_history/member_timestamp/row.rs b/src/session/view/content/room_history/member_timestamp/row.rs index 93636094..53d280ee 100644 --- a/src/session/view/content/room_history/member_timestamp/row.rs +++ b/src/session/view/content/room_history/member_timestamp/row.rs @@ -9,18 +9,19 @@ mod imp { use std::cell::RefCell; use glib::subclass::InitializingObject; - use once_cell::sync::Lazy; use super::*; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template( resource = "/org/gnome/Fractal/ui/session/view/content/room_history/member_timestamp/row.ui" )] + #[properties(wrapper_type = super::MemberTimestampRow)] pub struct MemberTimestampRow { #[template_child] pub timestamp: TemplateChild, /// The `MemberTimestamp` presented by this row. + #[property(get, set = Self::set_data, explicit_notify, nullable)] pub data: glib::WeakRef, pub system_settings_handler: RefCell>, } @@ -40,35 +41,8 @@ mod imp { } } + #[glib::derived_properties] impl ObjectImpl for MemberTimestampRow { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![glib::ParamSpecObject::builder::("data") - .explicit_notify() - .build()] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - - match pspec.name() { - "data" => obj.set_data(value.get::>().unwrap().as_ref()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "data" => obj.data().to_value(), - _ => unimplemented!(), - } - } - fn constructed(&self) { self.parent_constructed(); let obj = self.obj(); @@ -93,6 +67,21 @@ mod imp { impl WidgetImpl for MemberTimestampRow {} impl BinImpl for MemberTimestampRow {} + + impl MemberTimestampRow { + /// Set the `MemberTimestamp` presented by this row. + fn set_data(&self, data: Option) { + if self.data.upgrade() == data { + return; + } + let obj = self.obj(); + + self.data.set(data.as_ref()); + obj.notify_data(); + + obj.update_timestamp(); + } + } } glib::wrapper! { @@ -106,23 +95,6 @@ impl MemberTimestampRow { glib::Object::new() } - /// The `MemberTimestamp` presented by this row. - pub fn data(&self) -> Option { - self.imp().data.upgrade() - } - - /// Set the `MemberTimestamp` presented by this row. - pub fn set_data(&self, data: Option<&MemberTimestamp>) { - if self.data().as_ref() == data { - return; - } - - self.imp().data.set(data); - self.notify("data"); - - self.update_timestamp(); - } - /// The formatted date and time of this receipt. fn update_timestamp(&self) { let imp = self.imp(); diff --git a/src/session/view/content/room_history/message_row/audio.rs b/src/session/view/content/room_history/message_row/audio.rs index a06839e6..9d2dca91 100644 --- a/src/session/view/content/room_history/message_row/audio.rs +++ b/src/session/view/content/room_history/message_row/audio.rs @@ -19,20 +19,23 @@ mod imp { use std::cell::{Cell, RefCell}; use glib::subclass::InitializingObject; - use once_cell::sync::Lazy; use super::*; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template( resource = "/org/gnome/Fractal/ui/session/view/content/room_history/message_row/audio.ui" )] + #[properties(wrapper_type = super::MessageAudio)] pub struct MessageAudio { /// The body of the audio message. + #[property(get)] pub body: RefCell>, /// The state of the audio file. + #[property(get, builder(MediaState::default()))] pub state: Cell, /// Whether to display this audio message in a compact format. + #[property(get)] pub compact: Cell, #[template_child] pub player: TemplateChild, @@ -57,44 +60,10 @@ mod imp { } } - impl ObjectImpl for MessageAudio { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecString::builder("body").read_only().build(), - glib::ParamSpecEnum::builder::("state") - .explicit_notify() - .build(), - glib::ParamSpecBoolean::builder("compact") - .read_only() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - match pspec.name() { - "state" => self.obj().set_state(value.get().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "body" => obj.body().to_value(), - "state" => obj.state().to_value(), - "compact" => obj.compact().to_value(), - _ => unimplemented!(), - } - } - } + #[glib::derived_properties] + impl ObjectImpl for MessageAudio {} impl WidgetImpl for MessageAudio {} - impl BinImpl for MessageAudio {} } @@ -110,11 +79,6 @@ impl MessageAudio { glib::Object::new() } - /// The body of the audio message. - pub fn body(&self) -> Option { - self.imp().body.borrow().to_owned() - } - /// Set the body of the audio message. fn set_body(&self, body: Option) { if self.body() == body { @@ -122,12 +86,7 @@ impl MessageAudio { } self.imp().body.replace(body); - self.notify("body"); - } - - /// Whether to display this audio message in a compact format. - pub fn compact(&self) -> bool { - self.imp().compact.get() + self.notify_body(); } /// Set the compact format of this audio message. @@ -142,12 +101,7 @@ impl MessageAudio { self.add_css_class("toolbar"); } - self.notify("compact"); - } - - /// The state of the audio file. - pub fn state(&self) -> MediaState { - self.imp().state.get() + self.notify_compact(); } /// Set the state of the audio file. @@ -174,7 +128,7 @@ impl MessageAudio { } imp.state.set(state); - self.notify("state"); + self.notify_state(); } /// Convenience method to set the state to `Error` with the given error diff --git a/src/session/view/content/room_history/message_row/content.rs b/src/session/view/content/room_history/message_row/content.rs index 482cb23a..7724977f 100644 --- a/src/session/view/content/room_history/message_row/content.rs +++ b/src/session/view/content/room_history/message_row/content.rs @@ -39,12 +39,13 @@ pub enum ContentFormat { mod imp { use std::cell::Cell; - use once_cell::sync::Lazy; - use super::*; - #[derive(Debug, Default)] + #[derive(Debug, Default, glib::Properties)] + #[properties(wrapper_type = super::MessageContent)] pub struct MessageContent { + /// The displayed format of the message. + #[property(get, set = Self::set_format, explicit_notify, builder(ContentFormat::default()))] pub format: Cell, } @@ -55,37 +56,27 @@ mod imp { type ParentType = adw::Bin; } - impl ObjectImpl for MessageContent { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![glib::ParamSpecEnum::builder::("format") - .explicit_notify() - .build()] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - match pspec.name() { - "format" => self.obj().set_format(value.get().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - match pspec.name() { - "format" => self.obj().format().to_value(), - _ => unimplemented!(), - } - } - } + #[glib::derived_properties] + impl ObjectImpl for MessageContent {} impl WidgetImpl for MessageContent {} impl BinImpl for MessageContent {} + + impl MessageContent { + /// Set the displayed format of the message. + fn set_format(&self, format: ContentFormat) { + if self.format.get() == format { + return; + } + + self.format.set(format); + self.obj().notify_format(); + } + } } glib::wrapper! { + /// The content of a message in the timeline. pub struct MessageContent(ObjectSubclass) @extends gtk::Widget, adw::Bin, @implements gtk::Accessible; } @@ -95,21 +86,6 @@ impl MessageContent { glib::Object::new() } - /// The displayed format of the message. - pub fn format(&self) -> ContentFormat { - self.imp().format.get() - } - - /// Set the displayed format of the message. - pub fn set_format(&self, format: ContentFormat) { - if self.format() == format { - return; - } - - self.imp().format.set(format); - self.notify("format"); - } - /// Access the widget with the own content of the event. /// /// This allows to access the descendant content while discarding the diff --git a/src/session/view/content/room_history/message_row/file.rs b/src/session/view/content/room_history/message_row/file.rs index 2cba6a25..22d1ec07 100644 --- a/src/session/view/content/room_history/message_row/file.rs +++ b/src/session/view/content/room_history/message_row/file.rs @@ -7,18 +7,20 @@ mod imp { use std::cell::{Cell, RefCell}; use glib::subclass::InitializingObject; - use once_cell::sync::Lazy; use super::*; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template( resource = "/org/gnome/Fractal/ui/session/view/content/room_history/message_row/file.ui" )] + #[properties(wrapper_type = super::MessageFile)] pub struct MessageFile { - /// The filename of the file + /// The filename of the file. + #[property(get, set = Self::set_filename, explicit_notify, nullable)] pub filename: RefCell>, /// Whether this file should be displayed in a compact format. + #[property(get, set = Self::set_compact, explicit_notify)] pub compact: Cell, } @@ -37,50 +39,39 @@ mod imp { } } - impl ObjectImpl for MessageFile { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecString::builder("filename") - .explicit_notify() - .build(), - glib::ParamSpecBoolean::builder("compact") - .explicit_notify() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - - match pspec.name() { - "filename" => obj.set_filename(value.get().unwrap()), - "compact" => obj.set_compact(value.get().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "filename" => obj.filename().to_value(), - "compact" => obj.compact().to_value(), - _ => unimplemented!(), - } - } - } + #[glib::derived_properties] + impl ObjectImpl for MessageFile {} impl WidgetImpl for MessageFile {} - impl BinImpl for MessageFile {} + + impl MessageFile { + /// Set the filename of the file. + fn set_filename(&self, filename: Option) { + let filename = filename.filter(|s| !s.is_empty()); + + if filename == *self.filename.borrow() { + return; + } + + self.filename.replace(filename); + self.obj().notify_filename(); + } + + /// Set whether this file should be displayed in a compact format. + fn set_compact(&self, compact: bool) { + if self.compact.get() == compact { + return; + } + + self.compact.set(compact); + self.obj().notify_compact(); + } + } } glib::wrapper! { - /// A widget displaying an interface to download or open the content of a file message. + /// A widget displaying an interface to download the content of a file message. pub struct MessageFile(ObjectSubclass) @extends gtk::Widget, adw::Bin, @implements gtk::Accessible; } @@ -90,40 +81,6 @@ impl MessageFile { glib::Object::new() } - /// Set the filename of the file. - pub fn set_filename(&self, filename: Option) { - let imp = self.imp(); - - let name = filename.filter(|name| !name.is_empty()); - - if name.as_ref() == imp.filename.borrow().as_ref() { - return; - } - - imp.filename.replace(name); - self.notify("filename"); - } - - /// The filename of the file. - pub fn filename(&self) -> Option { - self.imp().filename.borrow().to_owned() - } - - /// Set whether this file should be displayed in a compact format. - pub fn set_compact(&self, compact: bool) { - if self.compact() == compact { - return; - } - - self.imp().compact.set(compact); - self.notify("compact"); - } - - /// Whether this file should be displayed in a compact format. - pub fn compact(&self) -> bool { - self.imp().compact.get() - } - pub fn set_format(&self, format: ContentFormat) { self.set_compact(matches!( format, diff --git a/src/session/view/content/room_history/message_row/media.rs b/src/session/view/content/room_history/message_row/media.rs index af3e3739..254e211a 100644 --- a/src/session/view/content/room_history/message_row/media.rs +++ b/src/session/view/content/room_history/message_row/media.rs @@ -56,22 +56,26 @@ mod imp { use std::cell::Cell; use glib::subclass::InitializingObject; - use once_cell::sync::Lazy; use super::*; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template( resource = "/org/gnome/Fractal/ui/session/view/content/room_history/message_row/media.ui" )] + #[properties(wrapper_type = super::MessageMedia)] pub struct MessageMedia { /// The intended display width of the media. + #[property(get, set = Self::set_width, explicit_notify, default = -1, minimum = -1)] pub width: Cell, /// The intended display height of the media. + #[property(get, set = Self::set_height, explicit_notify, default = -1, minimum = -1)] pub height: Cell, /// The state of the media. + #[property(get, builder(MediaState::default()))] pub state: Cell, /// Whether to display this media in a compact format. + #[property(get)] pub compact: Cell, #[template_child] pub media: TemplateChild, @@ -97,61 +101,8 @@ mod imp { } } + #[glib::derived_properties] impl ObjectImpl for MessageMedia { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecInt::builder("width") - .minimum(-1) - .default_value(-1) - .explicit_notify() - .build(), - glib::ParamSpecInt::builder("height") - .minimum(-1) - .default_value(-1) - .explicit_notify() - .build(), - glib::ParamSpecEnum::builder::("state") - .explicit_notify() - .build(), - glib::ParamSpecBoolean::builder("compact") - .read_only() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - - match pspec.name() { - "width" => { - obj.set_width(value.get().unwrap()); - } - "height" => { - obj.set_height(value.get().unwrap()); - } - "state" => { - obj.set_state(value.get().unwrap()); - } - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "width" => obj.width().to_value(), - "height" => obj.height().to_value(), - "state" => obj.state().to_value(), - "compact" => obj.compact().to_value(), - _ => unimplemented!(), - } - } - fn dispose(&self) { self.media.unparent(); } @@ -229,10 +180,32 @@ mod imp { } } } + + impl MessageMedia { + /// Set the intended display width of the media. + fn set_width(&self, width: i32) { + if self.width.get() == width { + return; + } + + self.width.set(width); + self.obj().notify_width(); + } + + /// Set the intended display height of the media. + fn set_height(&self, height: i32) { + if self.height.get() == height { + return; + } + + self.height.set(height); + self.obj().notify_height(); + } + } } glib::wrapper! { - /// A widget displaying a media message in the timeline. + /// A widget displaying a media (image or video) message in the timeline. pub struct MessageMedia(ObjectSubclass) @extends gtk::Widget, @implements gtk::Accessible; } @@ -250,43 +223,8 @@ impl MessageMedia { .unwrap(); } - /// The intended display width of the media. - pub fn width(&self) -> i32 { - self.imp().width.get() - } - - /// Set the intended display width of the media. - pub fn set_width(&self, width: i32) { - if self.width() == width { - return; - } - - self.imp().width.set(width); - self.notify("width"); - } - - /// The intended display height of the media. - pub fn height(&self) -> i32 { - self.imp().height.get() - } - - /// Set the intended display height of the media. - pub fn set_height(&self, height: i32) { - if self.height() == height { - return; - } - - self.imp().height.set(height); - self.notify("height"); - } - - /// The state of the media. - pub fn state(&self) -> MediaState { - self.imp().state.get() - } - /// Set the state of the media. - pub fn set_state(&self, state: MediaState) { + fn set_state(&self, state: MediaState) { let imp = self.imp(); if self.state() == state { @@ -309,18 +247,13 @@ impl MessageMedia { } imp.state.set(state); - self.notify("state"); - } - - /// Whether to display this media in a compact format. - pub fn compact(&self) -> bool { - self.imp().compact.get() + self.notify_state(); } /// Set whether to display this media in a compact format. fn set_compact(&self, compact: bool) { self.imp().compact.set(compact); - self.notify("compact"); + self.notify_compact(); } /// Display the given `image`, in a `compact` format or not. diff --git a/src/session/view/content/room_history/message_row/message_state_stack.rs b/src/session/view/content/room_history/message_row/message_state_stack.rs index 87c9ad0d..76ad5800 100644 --- a/src/session/view/content/room_history/message_row/message_state_stack.rs +++ b/src/session/view/content/room_history/message_row/message_state_stack.rs @@ -8,16 +8,17 @@ mod imp { use std::cell::Cell; use glib::subclass::InitializingObject; - use once_cell::sync::Lazy; use super::*; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template( resource = "/org/gnome/Fractal/ui/session/view/content/room_history/message_row/message_state_stack.ui" )] + #[properties(wrapper_type = super::MessageStateStack)] pub struct MessageStateStack { /// The state that is currently displayed. + #[property(get, set = Self::set_state, explicit_notify, builder(MessageState::default()))] pub state: Cell, #[template_child] pub stack: TemplateChild, @@ -40,39 +41,85 @@ mod imp { } } - impl ObjectImpl for MessageStateStack { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![glib::ParamSpecEnum::builder::("state") - .explicit_notify() - .build()] - }); + #[glib::derived_properties] + impl ObjectImpl for MessageStateStack {} - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - - match pspec.name() { - "state" => { - obj.set_state(value.get().unwrap()); - } - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "state" => obj.state().to_value(), - _ => unimplemented!(), - } - } - } impl WidgetImpl for MessageStateStack {} impl BinImpl for MessageStateStack {} + + impl MessageStateStack { + /// Set the state to display. + pub fn set_state(&self, state: MessageState) { + let prev_state = self.state.get(); + + if prev_state == state { + return; + } + + let obj = self.obj(); + let stack = &*self.stack; + + match state { + MessageState::None => { + if matches!( + prev_state, + MessageState::Sending | MessageState::Error | MessageState::Cancelled + ) { + // Show the sent icon for 2 seconds. + stack.set_visible_child_name("sent"); + + glib::timeout_add_seconds_local_once( + 2, + clone!(@weak obj => move || { + obj.set_visible(false); + }), + ); + } else { + obj.set_visible(false); + } + } + MessageState::Sending => { + stack.set_visible_child_name("sending"); + obj.set_visible(true); + } + MessageState::Error => { + self.error_image + .set_tooltip_text(Some(&gettext("Could not send the message"))); + stack.set_visible_child_name("error"); + obj.set_visible(true); + } + MessageState::Cancelled => { + self.error_image.set_tooltip_text(Some(&gettext( + "An error occurred with the sending queue", + ))); + stack.set_visible_child_name("error"); + obj.set_visible(true); + } + MessageState::Edited => { + if matches!( + prev_state, + MessageState::Sending | MessageState::Error | MessageState::Cancelled + ) { + // Show the sent icon for 2 seconds. + stack.set_visible_child_name("sent"); + + glib::timeout_add_seconds_local_once( + 2, + clone!(@weak stack => move || { + stack.set_visible_child_name("edited"); + }), + ); + } else { + stack.set_visible_child_name("edited"); + obj.set_visible(true); + } + } + } + + self.state.set(state); + obj.notify_state(); + } + } } glib::wrapper! { @@ -86,79 +133,4 @@ impl MessageStateStack { pub fn new() -> Self { glib::Object::new() } - - /// The state that is currently displayed. - pub fn state(&self) -> MessageState { - self.imp().state.get() - } - - /// Set the state to display. - pub fn set_state(&self, state: MessageState) { - let prev_state = self.state(); - - if prev_state == state { - return; - } - - let imp = self.imp(); - let stack = &*imp.stack; - match state { - MessageState::None => { - if matches!( - prev_state, - MessageState::Sending | MessageState::Error | MessageState::Cancelled - ) { - // Show the sent icon for 2 seconds. - stack.set_visible_child_name("sent"); - - glib::timeout_add_seconds_local_once( - 2, - clone!(@weak self as obj => move || { - obj.set_visible(false); - }), - ); - } else { - self.set_visible(false); - } - } - MessageState::Sending => { - stack.set_visible_child_name("sending"); - self.set_visible(true); - } - MessageState::Error => { - imp.error_image - .set_tooltip_text(Some(&gettext("Could not send the message"))); - stack.set_visible_child_name("error"); - self.set_visible(true); - } - MessageState::Cancelled => { - imp.error_image - .set_tooltip_text(Some(&gettext("An error occurred with the sending queue"))); - stack.set_visible_child_name("error"); - self.set_visible(true); - } - MessageState::Edited => { - if matches!( - prev_state, - MessageState::Sending | MessageState::Error | MessageState::Cancelled - ) { - // Show the sent icon for 2 seconds. - stack.set_visible_child_name("sent"); - - glib::timeout_add_seconds_local_once( - 2, - clone!(@weak stack => move || { - stack.set_visible_child_name("edited"); - }), - ); - } else { - stack.set_visible_child_name("edited"); - self.set_visible(true); - } - } - } - - imp.state.set(state); - self.notify("state"); - } } diff --git a/src/session/view/content/room_history/message_row/mod.rs b/src/session/view/content/room_history/message_row/mod.rs index 344d79dc..3e1fba86 100644 --- a/src/session/view/content/room_history/message_row/mod.rs +++ b/src/session/view/content/room_history/message_row/mod.rs @@ -25,17 +25,17 @@ use crate::{ }; mod imp { - use std::cell::RefCell; + use std::{cell::RefCell, marker::PhantomData}; use glib::subclass::InitializingObject; - use once_cell::sync::Lazy; use super::*; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template( resource = "/org/gnome/Fractal/ui/session/view/content/room_history/message_row/mod.ui" )] + #[properties(wrapper_type = super::MessageRow)] pub struct MessageRow { #[template_child] pub avatar: TemplateChild, @@ -55,7 +55,14 @@ mod imp { pub read_receipts: TemplateChild, pub bindings: RefCell>, pub system_settings_handler: RefCell>, + /// The event that is presented. + #[property(get, set = Self::set_event, explicit_notify)] pub event: BoundObject, + /// Whether this item should show its header. + /// + /// This is ignored if this event doesn’t have a header. + #[property(get = Self::show_header, set = Self::set_show_header, explicit_notify)] + pub show_header: PhantomData, } #[glib::object_subclass] @@ -77,53 +84,19 @@ mod imp { } } + #[glib::derived_properties] impl ObjectImpl for MessageRow { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecBoolean::builder("show-header") - .explicit_notify() - .build(), - glib::ParamSpecObject::builder::("event") - .explicit_notify() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - match pspec.name() { - "show-header" => obj.set_show_header(value.get().unwrap()), - "event" => obj.set_event(value.get().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - match pspec.name() { - "show-header" => obj.show_header().to_value(), - "event" => obj.event().to_value(), - _ => unimplemented!(), - } - } - fn constructed(&self) { self.parent_constructed(); let obj = self.obj(); - self.content.connect_notify_local( - Some("format"), - clone!(@weak self as imp => move |content, _| + self.content + .connect_format_notify(clone!(@weak self as imp => move |content| imp.reactions.set_visible(!matches!( content.format(), ContentFormat::Compact | ContentFormat::Ellipsized )); - ), - ); + )); let system_settings = Application::default().system_settings(); let system_settings_handler = system_settings.connect_notify_local( @@ -149,9 +122,94 @@ mod imp { impl WidgetImpl for MessageRow {} impl BinImpl for MessageRow {} + + impl MessageRow { + /// Whether this item should show its header. + /// + /// This is ignored if this event doesn’t have a header. + fn show_header(&self) -> bool { + self.avatar.is_visible() && self.header.is_visible() + } + + /// Set whether this item should show its header. + fn set_show_header(&self, visible: bool) { + let obj = self.obj(); + + self.avatar.set_visible(visible); + self.header.set_visible(visible); + + if let Some(row) = obj.parent() { + if visible { + row.add_css_class("has-header"); + } else { + row.remove_css_class("has-header"); + } + } + + obj.notify_show_header(); + } + + /// Set the event that is presented. + fn set_event(&self, event: Event) { + let Some(room) = event.room() else { + return; + }; + let obj = self.obj(); + + // Remove signals and bindings from the previous event. + self.event.disconnect_signals(); + while let Some(binding) = self.bindings.borrow_mut().pop() { + binding.unbind(); + } + + self.avatar + .set_data(Some(event.sender().avatar_data().clone())); + + let display_name_binding = event + .sender() + .bind_property("display-name", &*self.display_name, "label") + .sync_create() + .build(); + + let show_header_binding = event + .bind_property("show-header", &*obj, "show-header") + .sync_create() + .build(); + + let state_binding = event + .bind_property("state", &*self.message_state, "state") + .sync_create() + .build(); + + self.bindings.borrow_mut().append(&mut vec![ + display_name_binding, + show_header_binding, + state_binding, + ]); + + let timestamp_handler = event.connect_timestamp_notify(clone!(@weak obj => move |_| { + obj.update_timestamp(); + })); + + let source_handler = event.connect_source_notify(clone!(@weak obj => move |_| { + obj.update_content(); + })); + + self.reactions + .set_reaction_list(&room.get_or_create_members(), &event.reactions()); + self.read_receipts.set_source(&event.read_receipts()); + self.event + .set(event, vec![timestamp_handler, source_handler]); + obj.notify_event(); + + obj.update_content(); + obj.update_timestamp(); + } + } } glib::wrapper! { + /// A row displaying a message in the timeline. pub struct MessageRow(ObjectSubclass) @extends gtk::Widget, adw::Bin, @implements gtk::Accessible; } @@ -161,101 +219,10 @@ impl MessageRow { glib::Object::new() } - /// Whether this item should show its header. - /// - /// This is ignored if this event doesn’t have a header. - pub fn show_header(&self) -> bool { - let imp = self.imp(); - imp.avatar.is_visible() && imp.header.is_visible() - } - - /// Set whether this item should show its header. - pub fn set_show_header(&self, visible: bool) { - let imp = self.imp(); - imp.avatar.set_visible(visible); - imp.header.set_visible(visible); - - if let Some(row) = self.parent() { - if visible { - row.add_css_class("has-header"); - } else { - row.remove_css_class("has-header"); - } - } - - self.notify("show-header"); - } - pub fn set_content_format(&self, format: ContentFormat) { self.imp().content.set_format(format); } - pub fn event(&self) -> Option { - self.imp().event.obj() - } - - pub fn set_event(&self, event: Event) { - let Some(room) = event.room() else { - return; - }; - let imp = self.imp(); - - // Remove signals and bindings from the previous event. - imp.event.disconnect_signals(); - while let Some(binding) = imp.bindings.borrow_mut().pop() { - binding.unbind(); - } - - imp.avatar - .set_data(Some(event.sender().avatar_data().clone())); - - let display_name_binding = event - .sender() - .bind_property("display-name", &imp.display_name.get(), "label") - .sync_create() - .build(); - - let show_header_binding = event - .bind_property("show-header", self, "show-header") - .sync_create() - .build(); - - let state_binding = event - .bind_property("state", &*imp.message_state, "state") - .sync_create() - .build(); - - imp.bindings.borrow_mut().append(&mut vec![ - display_name_binding, - show_header_binding, - state_binding, - ]); - - let timestamp_handler = event.connect_notify_local( - Some("timestamp"), - clone!(@weak self as obj => move |_,_| { - obj.update_timestamp(); - }), - ); - - let source_handler = event.connect_notify_local( - Some("source"), - clone!(@weak self as obj => move |_, _| { - obj.update_content(); - }), - ); - - imp.reactions - .set_reaction_list(&room.get_or_create_members(), &event.reactions()); - imp.read_receipts.set_source(&event.read_receipts()); - imp.event - .set(event, vec![timestamp_handler, source_handler]); - self.notify("event"); - - self.update_content(); - self.update_timestamp(); - } - /// Update the displayed timestamp for the current event with the current /// clock format setting. fn update_timestamp(&self) { diff --git a/src/session/view/content/room_history/message_row/reaction/mod.rs b/src/session/view/content/room_history/message_row/reaction/mod.rs index fb1a6477..15a1d917 100644 --- a/src/session/view/content/room_history/message_row/reaction/mod.rs +++ b/src/session/view/content/room_history/message_row/reaction/mod.rs @@ -17,20 +17,23 @@ mod imp { use std::cell::RefCell; use glib::subclass::InitializingObject; - use once_cell::sync::Lazy; use super::*; - #[derive(Debug, CompositeTemplate)] + #[derive(Debug, CompositeTemplate, glib::Properties)] #[template( resource = "/org/gnome/Fractal/ui/session/view/content/room_history/message_row/reaction/mod.ui" )] + #[properties(wrapper_type = super::MessageReaction)] pub struct MessageReaction { /// The reaction senders (group) to display. + #[property(get, set = Self::set_group, construct_only)] pub group: BoundObjectWeakRef, /// The list of reaction senders as room members. + #[property(get)] pub list: gio::ListStore, /// The member list of the room of the reaction. + #[property(get, set = Self::set_members, explicit_notify, nullable)] pub members: RefCell>, #[template_child] pub button: TemplateChild, @@ -69,50 +72,63 @@ mod imp { } } - impl ObjectImpl for MessageReaction { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecObject::builder::("group") - .construct_only() - .build(), - glib::ParamSpecObject::builder::("list") - .read_only() - .build(), - glib::ParamSpecObject::builder::("members").build(), - ] - }); + #[glib::derived_properties] + impl ObjectImpl for MessageReaction {} - PROPERTIES.as_ref() - } + impl WidgetImpl for MessageReaction {} + impl FlowBoxChildImpl for MessageReaction {} - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - match pspec.name() { - "group" => { - self.obj().set_group(value.get().unwrap()); - } - "members" => self.obj().set_members(value.get().unwrap()), - _ => unimplemented!(), + impl MessageReaction { + /// Set the reaction group to display. + fn set_group(&self, group: ReactionGroup) { + let obj = self.obj(); + let key = group.key(); + self.reaction_key.set_label(&key); + + if EMOJI_REGEX.is_match(&key) { + self.reaction_key.add_css_class("emoji"); + } else { + self.reaction_key.remove_css_class("emoji"); } + + self.button.set_action_target_value(Some(&key.to_variant())); + group + .bind_property("has-user", &*self.button, "active") + .sync_create() + .build(); + group + .bind_property("count", &*self.reaction_count, "label") + .sync_create() + .build(); + + let items_changed_handler_id = + group.connect_items_changed(clone!(@weak obj => move |group, pos, removed, added| + obj.items_changed(group, pos, removed, added) + )); + obj.items_changed(&group, 0, self.list.n_items(), group.n_items()); + + self.group.set(&group, vec![items_changed_handler_id]); } - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - match pspec.name() { - "group" => self.obj().group().to_value(), - "list" => self.obj().list().to_value(), - "members" => self.obj().members().to_value(), - _ => unimplemented!(), + /// Set the members list of the room of the reaction. + fn set_members(&self, members: Option) { + if *self.members.borrow() == members { + return; + } + let obj = self.obj(); + + self.members.replace(members); + obj.notify_members(); + + if let Some(group) = self.group.obj() { + obj.items_changed(&group, 0, self.list.n_items(), group.n_items()); } } } - - impl WidgetImpl for MessageReaction {} - - impl FlowBoxChildImpl for MessageReaction {} } glib::wrapper! { - /// A widget displaying the reactions of a message. + /// A widget displaying a reaction of a message. pub struct MessageReaction(ObjectSubclass) @extends gtk::Widget, gtk::FlowBoxChild, @implements gtk::Accessible; } @@ -126,69 +142,6 @@ impl MessageReaction { .build() } - // The reaction group to display. - pub fn group(&self) -> Option { - self.imp().group.obj() - } - - /// Set the reaction group to display. - fn set_group(&self, group: ReactionGroup) { - let imp = self.imp(); - let key = group.key(); - imp.reaction_key.set_label(&key); - - if EMOJI_REGEX.is_match(&key) { - imp.reaction_key.add_css_class("emoji"); - } else { - imp.reaction_key.remove_css_class("emoji"); - } - - imp.button.set_action_target_value(Some(&key.to_variant())); - group - .bind_property("has-user", &*imp.button, "active") - .sync_create() - .build(); - group - .bind_property("count", &*imp.reaction_count, "label") - .sync_create() - .build(); - - let items_changed_handler_id = group.connect_items_changed( - clone!(@weak self as obj => move |group, pos, removed, added| - obj.items_changed(group, pos, removed, added) - ), - ); - self.items_changed(&group, 0, self.list().n_items(), group.n_items()); - - imp.group.set(&group, vec![items_changed_handler_id]); - } - - /// The list of reaction senders as room members. - pub fn list(&self) -> &gio::ListStore { - &self.imp().list - } - - /// The member list of the room of the reaction. - pub fn members(&self) -> Option { - self.imp().members.borrow().clone() - } - - /// Set the members list of the room of the reaction. - pub fn set_members(&self, members: Option) { - let imp = self.imp(); - - if imp.members.borrow().as_ref() == members.as_ref() { - return; - } - - imp.members.replace(members); - self.notify("members"); - - if let Some(group) = imp.group.obj() { - self.items_changed(&group, 0, self.list().n_items(), group.n_items()); - } - } - fn items_changed(&self, group: &ReactionGroup, pos: u32, removed: u32, added: u32) { let Some(members) = &*self.imp().members.borrow() else { return; @@ -216,13 +169,14 @@ impl MessageReaction { /// Shows a popover with the senders of that reaction, if there are any. #[template_callback] fn show_popover(&self) { - if self.list().n_items() == 0 { + let list = self.list(); + if list.n_items() == 0 { // No popover. return; }; let button = &*self.imp().button; - let popover = ReactionPopover::new(self.list()); + let popover = ReactionPopover::new(&list); popover.set_parent(button); popover.connect_closed(clone!(@weak button => move |popover| { popover.unparent(); diff --git a/src/session/view/content/room_history/message_row/reaction/reaction_popover.rs b/src/session/view/content/room_history/message_row/reaction/reaction_popover.rs index c7dbfc2a..75861d55 100644 --- a/src/session/view/content/room_history/message_row/reaction/reaction_popover.rs +++ b/src/session/view/content/room_history/message_row/reaction/reaction_popover.rs @@ -5,18 +5,19 @@ use crate::session::view::content::room_history::member_timestamp::row::MemberTi mod imp { use glib::subclass::InitializingObject; - use once_cell::sync::Lazy; use super::*; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template( resource = "/org/gnome/Fractal/ui/session/view/content/room_history/message_row/reaction/reaction_popover.ui" )] + #[properties(wrapper_type = super::ReactionPopover)] pub struct ReactionPopover { #[template_child] pub list: TemplateChild, /// The reaction senders to display. + #[property(get, set = Self::set_senders, construct_only)] pub senders: glib::WeakRef, } @@ -36,43 +37,20 @@ mod imp { } } - impl ObjectImpl for ReactionPopover { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![glib::ParamSpecObject::builder::("senders") - .construct_only() - .build()] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - - match pspec.name() { - "senders" => obj.set_senders(value.get().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "senders" => obj.senders().to_value(), - _ => unimplemented!(), - } - } - - fn constructed(&self) { - self.parent_constructed(); - } - } + #[glib::derived_properties] + impl ObjectImpl for ReactionPopover {} impl WidgetImpl for ReactionPopover {} - impl PopoverImpl for ReactionPopover {} + + impl ReactionPopover { + /// Set the reaction senders to display. + fn set_senders(&self, senders: gio::ListStore) { + self.senders.set(Some(&senders)); + self.list + .set_model(Some(>k::NoSelection::new(Some(senders)))); + } + } } glib::wrapper! { @@ -86,23 +64,4 @@ impl ReactionPopover { pub fn new(senders: &gio::ListStore) -> Self { glib::Object::builder().property("senders", senders).build() } - - /// The reaction senders to display. - pub fn senders(&self) -> Option { - self.imp().senders.upgrade() - } - - /// Set the reaction senders to display. - fn set_senders(&self, senders: Option) { - let Some(senders) = senders else { - // Ignore missing reaction senders. - return; - }; - let imp = self.imp(); - - imp.senders.set(Some(&senders)); - imp.list - .set_model(Some(>k::NoSelection::new(Some(senders)))); - self.notify("senders"); - } } diff --git a/src/session/view/content/room_history/message_row/reaction_list.rs b/src/session/view/content/room_history/message_row/reaction_list.rs index 9bd0ca85..d75998bc 100644 --- a/src/session/view/content/room_history/message_row/reaction_list.rs +++ b/src/session/view/content/room_history/message_row/reaction_list.rs @@ -35,9 +35,7 @@ mod imp { } impl ObjectImpl for MessageReactionList {} - impl WidgetImpl for MessageReactionList {} - impl BinImpl for MessageReactionList {} } diff --git a/src/session/view/content/room_history/message_row/reply.rs b/src/session/view/content/room_history/message_row/reply.rs index f63b9243..0f1be971 100644 --- a/src/session/view/content/room_history/message_row/reply.rs +++ b/src/session/view/content/room_history/message_row/reply.rs @@ -37,13 +37,12 @@ mod imp { } impl ObjectImpl for MessageReply {} - impl WidgetImpl for MessageReply {} - impl GridImpl for MessageReply {} } glib::wrapper! { + /// A widget displaying a reply to a message. pub struct MessageReply(ObjectSubclass) @extends gtk::Widget, gtk::Grid, @implements gtk::Accessible; } diff --git a/src/session/view/content/room_history/message_row/text.rs b/src/session/view/content/room_history/message_row/text.rs index 277c2588..8844a23e 100644 --- a/src/session/view/content/room_history/message_row/text.rs +++ b/src/session/view/content/room_history/message_row/text.rs @@ -24,19 +24,21 @@ enum WithMentions<'a> { mod imp { use std::cell::{Cell, RefCell}; - use once_cell::sync::Lazy; - use super::*; - #[derive(Debug, Default)] + #[derive(Debug, Default, glib::Properties)] + #[properties(wrapper_type = super::MessageText)] pub struct MessageText { /// The original text of the message that is displayed. + #[property(get)] pub original_text: RefCell, /// Whether the original text is HTML. /// /// Only used for emotes. + #[property(get)] pub is_html: Cell, /// The text format. + #[property(get, builder(ContentFormat::default()))] pub format: Cell, /// The sender of the message, if we need to listen to changes. pub sender: BoundObjectWeakRef, @@ -49,39 +51,10 @@ mod imp { type ParentType = adw::Bin; } - impl ObjectImpl for MessageText { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecString::builder("original-text") - .read_only() - .build(), - glib::ParamSpecBoolean::builder("is-html") - .read_only() - .build(), - glib::ParamSpecEnum::builder::("format") - .read_only() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "original-text" => obj.original_text().to_value(), - "is-html" => obj.is_html().to_value(), - "format" => obj.format().to_value(), - _ => unimplemented!(), - } - } - } + #[glib::derived_properties] + impl ObjectImpl for MessageText {} impl WidgetImpl for MessageText {} - impl BinImpl for MessageText {} } @@ -305,11 +278,6 @@ impl MessageText { } } - /// The original text of the message that is displayed. - pub fn original_text(&self) -> String { - self.imp().original_text.borrow().clone() - } - /// Whether the given text is different than the current original text. fn original_text_changed(&self, text: &str) -> bool { *self.imp().original_text.borrow() != text @@ -318,12 +286,7 @@ impl MessageText { /// Set the original text of the message to display. fn set_original_text(&self, text: String) { self.imp().original_text.replace(text); - self.notify("original-text"); - } - - /// Whether the original text of the message is HTML. - pub fn is_html(&self) -> bool { - self.imp().is_html.get() + self.notify_original_text(); } /// Set whether the original text of the message is HTML. @@ -333,12 +296,7 @@ impl MessageText { } self.imp().is_html.set(is_html); - self.notify("is-html"); - } - - /// The text format. - pub fn format(&self) -> ContentFormat { - self.imp().format.get() + self.notify_is_html(); } /// Whether the given format is different than the current format. @@ -349,7 +307,7 @@ impl MessageText { /// Set the text format. fn set_format(&self, format: ContentFormat) { self.imp().format.set(format); - self.notify("format"); + self.notify_format(); } /// Whether the sender of the message changed. diff --git a/src/session/view/content/room_history/message_toolbar/attachment_dialog.rs b/src/session/view/content/room_history/message_toolbar/attachment_dialog.rs index 7bab928a..dc64bc06 100644 --- a/src/session/view/content/room_history/message_toolbar/attachment_dialog.rs +++ b/src/session/view/content/room_history/message_toolbar/attachment_dialog.rs @@ -63,6 +63,7 @@ mod imp { } glib::wrapper! { + /// A dialog to preview an attachment before sending it. pub struct AttachmentDialog(ObjectSubclass) @extends gtk::Widget, gtk::Window, gtk::Root, adw::Window; } diff --git a/src/session/view/content/room_history/message_toolbar/completion/completion_popover.rs b/src/session/view/content/room_history/message_toolbar/completion/completion_popover.rs index 3e96b132..4cf2b920 100644 --- a/src/session/view/content/room_history/message_toolbar/completion/completion_popover.rs +++ b/src/session/view/content/room_history/message_toolbar/completion/completion_popover.rs @@ -18,25 +18,36 @@ use crate::{ const MAX_MEMBERS: usize = 32; mod imp { - use std::cell::{Cell, RefCell}; + use std::{ + cell::{Cell, RefCell}, + marker::PhantomData, + }; use glib::subclass::InitializingObject; - use once_cell::sync::Lazy; use super::*; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template( resource = "/org/gnome/Fractal/ui/session/view/content/room_history/message_toolbar/completion/completion_popover.ui" )] + #[properties(wrapper_type = super::CompletionPopover)] pub struct CompletionPopover { #[template_child] pub list: TemplateChild, + /// The parent `GtkTextView` to autocomplete. + #[property(get = Self::view)] + view: PhantomData, /// The user ID of the current session. + #[property(get, set = Self::set_user_id, explicit_notify, nullable)] pub user_id: RefCell>, /// The members list with expression watches. pub members_expr: ExpressionListModel, + /// The room members used for completion. + #[property(get = Self::members, set = Self::set_members, explicit_notify, nullable)] + members: PhantomData>, /// The sorted and filtered room members. + #[property(get)] pub filtered_members: gtk::FilterListModel, /// The rows in the popover. pub rows: [CompletionRow; MAX_MEMBERS], @@ -65,50 +76,8 @@ mod imp { } } + #[glib::derived_properties] impl ObjectImpl for CompletionPopover { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecObject::builder::("view") - .read_only() - .build(), - glib::ParamSpecString::builder("user-id") - .explicit_notify() - .build(), - glib::ParamSpecObject::builder::("members") - .explicit_notify() - .build(), - glib::ParamSpecObject::builder::("filtered-members") - .read_only() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - - match pspec.name() { - "user-id" => obj.set_user_id(value.get().unwrap()), - "members" => obj.set_members(value.get().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "view" => obj.view().to_value(), - "user-id" => obj.user_id().to_value(), - "members" => obj.members().to_value(), - "filtered-members" => obj.filtered_members().to_value(), - _ => unimplemented!(), - } - } - fn constructed(&self) { self.parent_constructed(); let obj = self.obj(); @@ -272,6 +241,38 @@ mod imp { impl WidgetImpl for CompletionPopover {} impl PopoverImpl for CompletionPopover {} + + impl CompletionPopover { + /// The parent `GtkTextView` to autocomplete. + fn view(&self) -> gtk::TextView { + self.obj().parent().and_downcast::().unwrap() + } + + /// Set the ID of the logged-in user. + fn set_user_id(&self, user_id: Option) { + if *self.user_id.borrow() == user_id { + return; + } + + self.user_id.replace(user_id); + self.obj().notify_user_id(); + } + + /// The room members used for completion. + fn members(&self) -> Option { + self.members_expr.model().and_downcast() + } + + /// Set the room members used for completion. + fn set_members(&self, members: Option) { + if self.members() == members { + return; + } + + self.members_expr.set_model(members.and_upcast()); + self.obj().notify_members(); + } + } } glib::wrapper! { @@ -285,48 +286,6 @@ impl CompletionPopover { glib::Object::new() } - /// The parent `GtkTextView` to autocomplete. - pub fn view(&self) -> gtk::TextView { - self.parent().and_downcast::().unwrap() - } - - /// The ID of the logged-in user. - pub fn user_id(&self) -> Option { - self.imp().user_id.borrow().clone() - } - - /// Set the ID of the logged-in user. - pub fn set_user_id(&self, user_id: Option) { - let imp = self.imp(); - - if imp.user_id.borrow().as_ref() == user_id.as_ref() { - return; - } - - imp.user_id.replace(user_id); - self.notify("user-id"); - } - - /// The room members used for completion. - pub fn members(&self) -> Option { - self.imp().members_expr.model().and_downcast() - } - - /// Set the room members used for completion. - pub fn set_members(&self, members: Option) { - if self.members() == members { - return; - } - - self.imp().members_expr.set_model(members.and_upcast()); - self.notify("members"); - } - - /// The sorted and filtered room members. - pub fn filtered_members(&self) -> >k::FilterListModel { - &self.imp().filtered_members - } - fn current_word(&self) -> Option<(gtk::TextIter, gtk::TextIter, String)> { self.imp().current_word.borrow().clone() } diff --git a/src/session/view/content/room_history/message_toolbar/completion/completion_row.rs b/src/session/view/content/room_history/message_toolbar/completion/completion_row.rs index d2d31d0a..a1ec2b4e 100644 --- a/src/session/view/content/room_history/message_toolbar/completion/completion_row.rs +++ b/src/session/view/content/room_history/message_toolbar/completion/completion_row.rs @@ -10,14 +10,14 @@ mod imp { use std::cell::RefCell; use glib::subclass::InitializingObject; - use once_cell::sync::Lazy; use super::*; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template( resource = "/org/gnome/Fractal/ui/session/view/content/room_history/message_toolbar/completion/completion_row.ui" )] + #[properties(wrapper_type = super::CompletionRow)] pub struct CompletionRow { #[template_child] pub avatar: TemplateChild, @@ -26,6 +26,7 @@ mod imp { #[template_child] pub id: TemplateChild, /// The room member presented by this row. + #[property(get, set = Self::set_member, explicit_notify, nullable)] pub member: RefCell>, } @@ -44,34 +45,33 @@ mod imp { } } - impl ObjectImpl for CompletionRow { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![glib::ParamSpecObject::builder::("member") - .explicit_notify() - .build()] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - match pspec.name() { - "member" => self.obj().set_member(value.get().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - match pspec.name() { - "member" => self.obj().member().to_value(), - _ => unimplemented!(), - } - } - } + #[glib::derived_properties] + impl ObjectImpl for CompletionRow {} impl WidgetImpl for CompletionRow {} impl ListBoxRowImpl for CompletionRow {} + + impl CompletionRow { + /// Set the room member displayed by this row. + fn set_member(&self, member: Option) { + if *self.member.borrow() == member { + return; + } + + if let Some(member) = &member { + self.avatar.set_data(Some(member.avatar_data().to_owned())); + self.display_name.set_label(&member.display_name()); + self.id.set_label(member.user_id().as_str()); + } else { + self.avatar.set_data(None::); + self.display_name.set_label(""); + self.id.set_label(""); + } + + self.member.replace(member); + self.obj().notify_member(); + } + } } glib::wrapper! { @@ -84,33 +84,6 @@ impl CompletionRow { pub fn new() -> Self { glib::Object::new() } - - /// The room member displayed by this row. - pub fn member(&self) -> Option { - self.imp().member.borrow().clone() - } - - /// Set the room member displayed by this row. - pub fn set_member(&self, member: Option) { - let imp = self.imp(); - - if imp.member.borrow().as_ref() == member.as_ref() { - return; - } - - if let Some(member) = &member { - imp.avatar.set_data(Some(member.avatar_data().to_owned())); - imp.display_name.set_label(&member.display_name()); - imp.id.set_label(member.user_id().as_str()); - } else { - imp.avatar.set_data(Option::::None); - imp.display_name.set_label(""); - imp.id.set_label(""); - } - - imp.member.replace(member); - self.notify("member"); - } } impl Default for CompletionRow { diff --git a/src/session/view/content/room_history/message_toolbar/mod.rs b/src/session/view/content/room_history/message_toolbar/mod.rs index 0dc6fdcc..a2f0e5e4 100644 --- a/src/session/view/content/room_history/message_toolbar/mod.rs +++ b/src/session/view/content/room_history/message_toolbar/mod.rs @@ -68,17 +68,23 @@ mod imp { use super::*; use crate::Application; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template( resource = "/org/gnome/Fractal/ui/session/view/content/room_history/message_toolbar/mod.ui" )] + #[properties(wrapper_type = super::MessageToolbar)] pub struct MessageToolbar { + /// The room to send messages in. + #[property(get, set = Self::set_room, explicit_notify, nullable)] pub room: glib::WeakRef, /// Whether our own user can send messages in the current room. + #[property(get)] pub can_send_messages: Cell, pub own_member: glib::WeakRef, pub power_levels_handler: RefCell>, - pub md_enabled: Cell, + /// Whether outgoing messages should be interpreted as markdown. + #[property(get, set)] + pub markdown_enabled: Cell, pub completion: CompletionPopover, #[template_child] pub message_entry: TemplateChild, @@ -86,7 +92,11 @@ mod imp { pub related_event_header: TemplateChild, #[template_child] pub related_event_content: TemplateChild, + /// The type of related event of the composer. + #[property(get, builder(RelatedEventType::default()))] pub related_event_type: Cell, + /// The related event of the composer. + #[property(get)] pub related_event: RefCell>, } @@ -156,55 +166,8 @@ mod imp { } } + #[glib::derived_properties] impl ObjectImpl for MessageToolbar { - fn properties() -> &'static [glib::ParamSpec] { - use once_cell::sync::Lazy; - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecObject::builder::("room") - .explicit_notify() - .build(), - glib::ParamSpecBoolean::builder("can-send-messages") - .read_only() - .build(), - glib::ParamSpecBoolean::builder("markdown-enabled") - .explicit_notify() - .build(), - glib::ParamSpecEnum::builder::("related-event-type") - .read_only() - .build(), - glib::ParamSpecObject::builder::("related-event") - .read_only() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - - match pspec.name() { - "room" => obj.set_room(value.get::>().unwrap().as_ref()), - "markdown-enabled" => obj.set_markdown_enabled(value.get().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "room" => obj.room().to_value(), - "can-send-messages" => obj.can_send_messages().to_value(), - "markdown-enabled" => obj.markdown_enabled().to_value(), - "related-event-type" => obj.related_event_type().to_value(), - "related-event" => obj.related_event().to_value(), - _ => unimplemented!(), - } - } - fn constructed(&self) { self.parent_constructed(); let obj = self.obj(); @@ -299,6 +262,33 @@ mod imp { impl WidgetImpl for MessageToolbar {} impl BoxImpl for MessageToolbar {} + + impl MessageToolbar { + /// Set the room currently displayed. + fn set_room(&self, room: Option) { + let old_room = self.room.upgrade(); + if old_room == room { + return; + } + let obj = self.obj(); + + if let Some(room) = old_room { + if let Some(handler) = self.power_levels_handler.take() { + room.power_levels().disconnect(handler); + } + } + + obj.clear_related_event(); + + self.room.set(room.as_ref()); + + obj.update_completion(room.as_ref()); + obj.set_up_can_send_messages(room.as_ref()); + self.message_entry.grab_focus(); + + obj.notify_room(); + } + } } glib::wrapper! { @@ -313,61 +303,11 @@ impl MessageToolbar { glib::Object::new() } - /// The room to send messages in. - pub fn room(&self) -> Option { - self.imp().room.upgrade() - } - - /// Set the room currently displayed. - pub fn set_room(&self, room: Option<&Room>) { - let old_room = self.room(); - if old_room.as_ref() == room { - return; - } - - let imp = self.imp(); - - if let Some(room) = old_room { - if let Some(handler) = imp.power_levels_handler.take() { - room.power_levels().disconnect(handler); - } - } - - self.clear_related_event(); - - imp.room.set(room); - - self.update_completion(room); - self.set_up_can_send_messages(room); - imp.message_entry.grab_focus(); - - self.notify("room"); - } - /// The `Member` for our own user in the current room. pub fn own_member(&self) -> Option { self.imp().own_member.upgrade() } - /// Whether outgoing messages should be interpreted as markdown. - pub fn markdown_enabled(&self) -> bool { - self.imp().md_enabled.get() - } - - /// Set whether outgoing messages should be interpreted as markdown. - pub fn set_markdown_enabled(&self, enabled: bool) { - let imp = self.imp(); - - imp.md_enabled.set(enabled); - - self.notify("markdown-enabled"); - } - - /// The type of related event of the composer. - pub fn related_event_type(&self) -> RelatedEventType { - self.imp().related_event_type.get() - } - /// Set the type of related event of the composer. fn set_related_event_type(&self, related_type: RelatedEventType) { if self.related_event_type() == related_type { @@ -375,12 +315,7 @@ impl MessageToolbar { } self.imp().related_event_type.set(related_type); - self.notify("related-event-type"); - } - - /// The related event of the composer. - pub fn related_event(&self) -> Option { - self.imp().related_event.borrow().clone() + self.notify_related_event_type(); } /// Set the related event of the composer. @@ -399,7 +334,7 @@ impl MessageToolbar { } self.imp().related_event.replace(event); - self.notify("related-event"); + self.notify_related_event(); } pub fn clear_related_event(&self) { @@ -531,7 +466,7 @@ impl MessageToolbar { let (start_iter, end_iter) = buffer.bounds(); let body_len = buffer.text(&start_iter, &end_iter, true).len(); - let is_markdown = imp.md_enabled.get(); + let is_markdown = self.markdown_enabled(); let mut has_mentions = false; let mut plain_body = String::with_capacity(body_len); // formatted_body is Markdown if is_markdown is true, and HTML if false. @@ -913,11 +848,6 @@ impl MessageToolbar { } } - /// Whether our own user can send messages in the current room. - pub fn can_send_messages(&self) -> bool { - self.imp().can_send_messages.get() - } - /// Update whether our own user can send messages in the current room. fn update_can_send_messages(&self) { let can_send = self.compute_can_send_messages(); @@ -928,7 +858,7 @@ impl MessageToolbar { self.imp().can_send_messages.set(can_send); self.set_sensitive(can_send); - self.notify("can-send-messages"); + self.notify_can_send_messages(); } fn set_up_can_send_messages(&self, room: Option<&Room>) { @@ -948,9 +878,8 @@ impl MessageToolbar { })); imp.own_member.set(Some(&own_member)); - let power_levels_handler = room.power_levels().connect_notify_local( - Some("power-levels"), - clone!(@weak self as obj => move |_, _| { + let power_levels_handler = room.power_levels().connect_power_levels_notify( + clone!(@weak self as obj => move |_| { obj.update_can_send_messages(); }), ); diff --git a/src/session/view/content/room_history/mod.rs b/src/session/view/content/room_history/mod.rs index 09a6ac1a..1d98d2c5 100644 --- a/src/session/view/content/room_history/mod.rs +++ b/src/session/view/content/room_history/mod.rs @@ -49,6 +49,7 @@ mod imp { use std::{ cell::{Cell, RefCell}, collections::HashMap, + marker::PhantomData, }; use glib::{signal::SignalHandlerId, subclass::InitializingObject}; @@ -56,16 +57,27 @@ mod imp { use super::*; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template(resource = "/org/gnome/Fractal/ui/session/view/content/room_history/mod.ui")] + #[properties(wrapper_type = super::RoomHistory)] pub struct RoomHistory { + /// The room currently displayed. + #[property(get, set = Self::set_room, explicit_notify, nullable)] pub room: RefCell>, /// Whether this is the only view visible, i.e. there is no sidebar. + #[property(get, set)] pub only_view: Cell, + /// Whether this `RoomHistory` is empty, aka no room is currently + /// displayed. + #[property(get = Self::empty)] + empty: PhantomData, pub room_members: RefCell>, pub room_handlers: RefCell>, pub timeline_handlers: RefCell>, pub is_auto_scrolling: Cell, + /// Whether the room history should stick to the newest message in the + /// timeline. + #[property(get, set = Self::set_sticky, explicit_notify)] pub sticky: Cell, pub item_context_menu: OnceCell, pub item_reaction_chooser: ReactionChooser, @@ -193,50 +205,8 @@ mod imp { } } + #[glib::derived_properties] impl ObjectImpl for RoomHistory { - fn properties() -> &'static [glib::ParamSpec] { - use once_cell::sync::Lazy; - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecObject::builder::("room") - .explicit_notify() - .build(), - glib::ParamSpecBoolean::builder("only-view").build(), - glib::ParamSpecBoolean::builder("empty") - .explicit_notify() - .build(), - glib::ParamSpecBoolean::builder("sticky") - .explicit_notify() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - - match pspec.name() { - "room" => obj.set_room(value.get().unwrap()), - "only-view" => self.only_view.set(value.get().unwrap()), - "sticky" => obj.set_sticky(value.get().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "room" => obj.room().to_value(), - "only-view" => self.only_view.get().to_value(), - "empty" => obj.is_empty().to_value(), - "sticky" => obj.sticky().to_value(), - _ => unimplemented!(), - } - } - fn constructed(&self) { self.setup_listview(); self.setup_drop_target(); @@ -376,9 +346,134 @@ mod imp { self.drag_overlay.set_drop_target(target); } } + + impl RoomHistory { + /// Set the room currently displayed. + fn set_room(&self, room: Option) { + if *self.room.borrow() == room { + return; + } + let obj = self.obj(); + + if let Some(room) = &*self.room.borrow() { + for handler in self.room_handlers.take() { + room.disconnect(handler); + } + + for handler in self.timeline_handlers.take() { + room.timeline().disconnect(handler); + } + + for (_, expr_watch) in self.room_expr_watches.take() { + expr_watch.unwatch(); + } + } + + if let Some(source_id) = self.scroll_timeout.take() { + source_id.remove(); + } + if let Some(source_id) = self.read_timeout.take() { + source_id.remove(); + } + + if let Some(room) = &room { + let timeline = room.timeline(); + + let category_handler = room.connect_category_notify(clone!(@weak obj => move |_| { + obj.update_room_state(); + })); + + let tombstoned_handler = + room.connect_is_tombstoned_notify(clone!(@weak obj => move |_| { + obj.update_tombstoned_banner(); + })); + + let successor_handler = + room.connect_successor_id_string_notify(clone!(@weak obj => move |_| { + obj.update_tombstoned_banner(); + })); + + let successor_room_handler = + room.connect_successor_notify(clone!(@weak obj => move |_| { + obj.update_tombstoned_banner(); + })); + + self.room_handlers.replace(vec![ + category_handler, + tombstoned_handler, + successor_handler, + successor_room_handler, + ]); + + let empty_handler = timeline.connect_empty_notify(clone!(@weak obj => move |_| { + obj.update_view(); + })); + + let state_handler = + timeline.connect_state_notify(clone!(@weak obj => move |timeline| { + obj.update_view(); + + // Always test if we need to load more when timeline is ready. + if timeline.state() == TimelineState::Ready { + obj.start_loading(); + } + })); + + self.timeline_handlers + .replace(vec![empty_handler, state_handler]); + + timeline.remove_empty_typing_row(); + obj.trigger_read_receipts_update(); + + obj.init_invite_action(room); + obj.scroll_down(); + } + + // Keep a strong reference to the members list before changing the model, so all + // events use the same list. + self.room_members + .replace(room.as_ref().map(|r| r.get_or_create_members())); + + let model = room.as_ref().map(|room| room.timeline().items()); + obj.selection_model().set_model(model.as_ref()); + + self.is_loading.set(false); + self.room.replace(room); + obj.update_view(); + obj.start_loading(); + obj.update_room_state(); + obj.update_tombstoned_banner(); + + obj.notify_room(); + obj.notify_empty(); + } + + /// Whether this `RoomHistory` is empty, aka no room is currently + /// displayed. + fn empty(&self) -> bool { + self.room.borrow().is_none() + } + + /// Set whether the room history should stick to the newest message in + /// the timeline. + fn set_sticky(&self, sticky: bool) { + if self.sticky.get() == sticky { + return; + } + + if !sticky { + self.scroll_btn_revealer.set_visible(true); + } + self.scroll_btn_revealer.set_reveal_child(!sticky); + + self.sticky.set(sticky); + self.obj().notify_sticky(); + } + } } glib::wrapper! { + /// A view that displays the timeline of a room and ways to send new messages. pub struct RoomHistory(ObjectSubclass) @extends gtk::Widget, adw::Bin, @implements gtk::Accessible; } @@ -393,136 +488,11 @@ impl RoomHistory { &self.imp().message_toolbar } - /// Set the room currently displayed. - pub fn set_room(&self, room: Option) { - let imp = self.imp(); - - if self.room() == room { - return; - } - - if let Some(room) = self.room() { - for handler in imp.room_handlers.take() { - room.disconnect(handler); - } - - for handler in imp.timeline_handlers.take() { - room.timeline().disconnect(handler); - } - - for (_, expr_watch) in imp.room_expr_watches.take() { - expr_watch.unwatch(); - } - } - - if let Some(source_id) = imp.scroll_timeout.take() { - source_id.remove(); - } - if let Some(source_id) = imp.read_timeout.take() { - source_id.remove(); - } - - if let Some(ref room) = room { - let timeline = room.timeline(); - - let category_handler = room.connect_notify_local( - Some("category"), - clone!(@weak self as obj => move |_, _| { - obj.update_room_state(); - }), - ); - - let tombstoned_handler = room.connect_notify_local( - Some("tombstoned"), - clone!(@weak self as obj => move |_, _| { - obj.update_tombstoned_banner(); - }), - ); - - let successor_handler = room.connect_notify_local( - Some("successor"), - clone!(@weak self as obj => move |_, _| { - obj.update_tombstoned_banner(); - }), - ); - - let successor_room_handler = room.connect_notify_local( - Some("successor-room"), - clone!(@weak self as obj => move |_, _| { - obj.update_tombstoned_banner(); - }), - ); - - imp.room_handlers.replace(vec![ - category_handler, - tombstoned_handler, - successor_handler, - successor_room_handler, - ]); - - let empty_handler = timeline.connect_notify_local( - Some("empty"), - clone!(@weak self as obj => move |_, _| { - obj.update_view(); - }), - ); - - let state_handler = timeline.connect_notify_local( - Some("state"), - clone!(@weak self as obj => move |timeline, _| { - obj.update_view(); - - // Always test if we need to load more when timeline is ready. - if timeline.state() == TimelineState::Ready { - obj.start_loading(); - } - }), - ); - - imp.timeline_handlers - .replace(vec![empty_handler, state_handler]); - - timeline.remove_empty_typing_row(); - self.trigger_read_receipts_update(); - - self.init_invite_action(room); - self.scroll_down(); - } - - // Keep a strong reference to the members list before changing the model, so all - // events use the same list. - imp.room_members - .replace(room.as_ref().map(|r| r.get_or_create_members())); - - let model = room.as_ref().map(|room| room.timeline().items()); - self.selection_model().set_model(model.as_ref()); - - imp.is_loading.set(false); - imp.room.replace(room); - self.update_view(); - self.start_loading(); - self.update_room_state(); - self.update_tombstoned_banner(); - - self.notify("room"); - self.notify("empty"); - } - - /// The room currently displayed. - pub fn room(&self) -> Option { - self.imp().room.borrow().clone() - } - /// The members of the room currently displayed. pub fn room_members(&self) -> Option { self.imp().room_members.borrow().clone() } - /// Whether this `RoomHistory` is empty, aka no room is currently displayed. - pub fn is_empty(&self) -> bool { - self.imp().room.borrow().is_none() - } - fn selection_model(&self) -> >k::NoSelection { self.imp() .selection_model @@ -699,30 +669,6 @@ impl RoomHistory { self.root().and_downcast() } - /// Whether the room history should stick to the newest message in the - /// timeline. - pub fn sticky(&self) -> bool { - self.imp().sticky.get() - } - - /// Set whether the room history should stick to the newest message in the - /// timeline. - pub fn set_sticky(&self, sticky: bool) { - let imp = self.imp(); - - if self.sticky() == sticky { - return; - } - - if !sticky { - imp.scroll_btn_revealer.set_visible(true); - } - imp.scroll_btn_revealer.set_reveal_child(!sticky); - - imp.sticky.set(sticky); - self.notify("sticky"); - } - /// Scroll to the newest message in the timeline pub fn scroll_down(&self) { let imp = self.imp(); diff --git a/src/session/view/content/room_history/mod.ui b/src/session/view/content/room_history/mod.ui index fd83a5aa..fa1e75ef 100644 --- a/src/session/view/content/room_history/mod.ui +++ b/src/session/view/content/room_history/mod.ui @@ -72,7 +72,7 @@ - + ContentRoomHistory diff --git a/src/session/view/content/room_history/read_receipts_list/mod.rs b/src/session/view/content/room_history/read_receipts_list/mod.rs index 5865cf89..54268167 100644 --- a/src/session/view/content/room_history/read_receipts_list/mod.rs +++ b/src/session/view/content/room_history/read_receipts_list/mod.rs @@ -21,14 +21,14 @@ mod imp { use std::cell::{Cell, RefCell}; use glib::subclass::InitializingObject; - use once_cell::sync::Lazy; use super::*; - #[derive(Debug, CompositeTemplate)] + #[derive(Debug, CompositeTemplate, glib::Properties)] #[template( resource = "/org/gnome/Fractal/ui/session/view/content/room_history/read_receipts_list/mod.ui" )] + #[properties(wrapper_type = super::ReadReceiptsList)] pub struct ReadReceiptsList { #[template_child] pub label: TemplateChild, @@ -37,10 +37,13 @@ mod imp { /// Whether this list is active. /// /// This list is active when the popover is displayed. + #[property(get)] pub active: Cell, /// The list of room members. + #[property(get, set = Self::set_members, explicit_notify, nullable)] pub members: RefCell>, /// The list of read receipts. + #[property(get)] pub list: gio::ListStore, /// The read receipts used as a source. pub source: BoundObjectWeakRef, @@ -81,43 +84,8 @@ mod imp { } } + #[glib::derived_properties] impl ObjectImpl for ReadReceiptsList { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecBoolean::builder("active") - .read_only() - .build(), - glib::ParamSpecObject::builder::("members").build(), - glib::ParamSpecObject::builder::("list") - .read_only() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "active" => obj.active().to_value(), - "members" => obj.members().to_value(), - "list" => obj.list().to_value(), - _ => unimplemented!(), - } - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - - match pspec.name() { - "members" => obj.set_members(value.get().unwrap()), - _ => unimplemented!(), - } - } - fn constructed(&self) { self.parent_constructed(); let obj = self.obj(); @@ -149,6 +117,23 @@ mod imp { None } } + + impl ReadReceiptsList { + /// Set the list of room members. + fn set_members(&self, members: Option) { + if *self.members.borrow() == members { + return; + } + let obj = self.obj(); + + self.members.replace(members); + obj.notify_members(); + + if let Some(source) = self.source.obj() { + obj.items_changed(&source, 0, self.list.n_items(), source.n_items()); + } + } + } } glib::wrapper! { @@ -163,13 +148,6 @@ impl ReadReceiptsList { glib::Object::builder().property("members", members).build() } - /// Whether this list is active. - /// - /// This list is active when the popover is displayed. - pub fn active(&self) -> bool { - self.imp().active.get() - } - /// Set whether this list is active. fn set_active(&self, active: bool) { if self.active() == active { @@ -177,7 +155,7 @@ impl ReadReceiptsList { } self.imp().active.set(active); - self.notify("active"); + self.notify_active(); self.set_pressed_state(active); } @@ -197,32 +175,6 @@ impl ReadReceiptsList { self.update_state(&[gtk::accessible::State::Pressed(tristate)]); } - /// The list of room members. - pub fn members(&self) -> Option { - self.imp().members.borrow().clone() - } - - /// Set the list of room members. - pub fn set_members(&self, members: Option) { - let imp = self.imp(); - - if imp.members.borrow().as_ref() == members.as_ref() { - return; - } - - imp.members.replace(members); - self.notify("members"); - - if let Some(source) = imp.source.obj() { - self.items_changed(&source, 0, self.list().n_items(), source.n_items()); - } - } - - /// The list of read receipts to present. - pub fn list(&self) -> &gio::ListStore { - &self.imp().list - } - /// Set the read receipts that are used as a source of data. pub fn set_source(&self, source: &gio::ListStore) { let imp = self.imp(); @@ -327,13 +279,14 @@ impl ReadReceiptsList { /// Shows a popover with the list of receipts if there are any. #[template_callback] fn show_popover(&self, _n_press: i32, x: f64, y: f64) { - if self.list().n_items() == 0 { + let list = self.list(); + if list.n_items() == 0 { // No popover. return; } self.set_active(true); - let popover = ReadReceiptsPopover::new(self.list()); + let popover = ReadReceiptsPopover::new(&list); popover.set_parent(self); popover.set_pointing_to(Some(&gdk::Rectangle::new(x as i32, y as i32, 0, 0))); popover.connect_closed(clone!(@weak self as obj => move |popover| { diff --git a/src/session/view/content/room_history/read_receipts_list/read_receipts_popover.rs b/src/session/view/content/room_history/read_receipts_list/read_receipts_popover.rs index 77667195..3da2fe4e 100644 --- a/src/session/view/content/room_history/read_receipts_list/read_receipts_popover.rs +++ b/src/session/view/content/room_history/read_receipts_list/read_receipts_popover.rs @@ -4,18 +4,19 @@ use crate::session::view::content::room_history::member_timestamp::row::MemberTi mod imp { use glib::subclass::InitializingObject; - use once_cell::sync::Lazy; use super::*; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template( resource = "/org/gnome/Fractal/ui/session/view/content/room_history/read_receipts_list/read_receipts_popover.ui" )] + #[properties(wrapper_type = super::ReadReceiptsPopover)] pub struct ReadReceiptsPopover { #[template_child] pub list: TemplateChild, /// The receipts to display. + #[property(get, set = Self::set_receipts, construct_only)] pub receipts: glib::WeakRef, } @@ -35,35 +36,8 @@ mod imp { } } + #[glib::derived_properties] impl ObjectImpl for ReadReceiptsPopover { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![glib::ParamSpecObject::builder::("receipts") - .construct_only() - .build()] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - - match pspec.name() { - "receipts" => obj.set_receipts(value.get().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "receipts" => obj.receipts().to_value(), - _ => unimplemented!(), - } - } - fn constructed(&self) { self.parent_constructed(); } @@ -71,6 +45,15 @@ mod imp { impl WidgetImpl for ReadReceiptsPopover {} impl PopoverImpl for ReadReceiptsPopover {} + + impl ReadReceiptsPopover { + /// Set the receipts to display. + fn set_receipts(&self, receipts: gio::ListStore) { + self.receipts.set(Some(&receipts)); + self.list + .set_model(Some(>k::NoSelection::new(Some(receipts)))); + } + } } glib::wrapper! { @@ -86,23 +69,4 @@ impl ReadReceiptsPopover { .property("receipts", receipts) .build() } - - /// The receipts to display. - pub fn receipts(&self) -> Option { - self.imp().receipts.upgrade() - } - - /// Set the receipts to display. - fn set_receipts(&self, receipts: Option) { - let Some(receipts) = receipts else { - // Ignore missing receipts. - return; - }; - let imp = self.imp(); - - imp.receipts.set(Some(&receipts)); - imp.list - .set_model(Some(>k::NoSelection::new(Some(receipts)))); - self.notify("receipts"); - } } diff --git a/src/session/view/content/room_history/state_row/creation.rs b/src/session/view/content/room_history/state_row/creation.rs index 66bc2936..b384af47 100644 --- a/src/session/view/content/room_history/state_row/creation.rs +++ b/src/session/view/content/room_history/state_row/creation.rs @@ -41,6 +41,7 @@ mod imp { } glib::wrapper! { + /// A widget presenting a room create state event. pub struct StateCreation(ObjectSubclass) @extends gtk::Widget, adw::Bin, @implements gtk::Accessible; } diff --git a/src/session/view/content/room_history/state_row/mod.rs b/src/session/view/content/room_history/state_row/mod.rs index d23b69bb..e4b6d5b2 100644 --- a/src/session/view/content/room_history/state_row/mod.rs +++ b/src/session/view/content/room_history/state_row/mod.rs @@ -22,21 +22,22 @@ mod imp { use std::cell::RefCell; use glib::subclass::InitializingObject; - use once_cell::sync::Lazy; use super::*; use crate::utils::template_callbacks::TemplateCallbacks; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template( resource = "/org/gnome/Fractal/ui/session/view/content/room_history/state_row/mod.ui" )] + #[properties(wrapper_type = super::StateRow)] pub struct StateRow { #[template_child] pub content: TemplateChild, #[template_child] pub read_receipts: TemplateChild, /// The state event displayed by this widget. + #[property(get, set = Self::set_event)] pub event: RefCell>, } @@ -56,39 +57,38 @@ mod imp { } } - impl ObjectImpl for StateRow { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![glib::ParamSpecObject::builder::("event") - .explicit_notify() - .build()] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - match pspec.name() { - "event" => obj.set_event(value.get().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - match pspec.name() { - "event" => obj.event().to_value(), - _ => unimplemented!(), - } - } - } + #[glib::derived_properties] + impl ObjectImpl for StateRow {} impl WidgetImpl for StateRow {} impl BinImpl for StateRow {} + + impl StateRow { + /// Set the event presented by this row. + fn set_event(&self, event: Event) { + let obj = self.obj(); + + match event.content() { + TimelineItemContent::MembershipChange(membership_change) => { + obj.update_with_membership_change(&membership_change, &event.sender_id()) + } + TimelineItemContent::ProfileChange(profile_change) => { + obj.update_with_profile_change(&profile_change, &event.sender().display_name()) + } + TimelineItemContent::OtherState(other_state) => { + obj.update_with_other_state(&event, &other_state) + } + _ => unreachable!(), + } + + self.read_receipts.set_source(&event.read_receipts()); + self.event.replace(Some(event)); + } + } } glib::wrapper! { + /// A row presenting a state event. pub struct StateRow(ObjectSubclass) @extends gtk::Widget, adw::Bin, @implements gtk::Accessible; } @@ -102,30 +102,6 @@ impl StateRow { &self.imp().content } - pub fn event(&self) -> Option { - self.imp().event.borrow().clone() - } - - pub fn set_event(&self, event: Event) { - match event.content() { - TimelineItemContent::MembershipChange(membership_change) => { - self.update_with_membership_change(&membership_change, &event.sender_id()) - } - TimelineItemContent::ProfileChange(profile_change) => { - self.update_with_profile_change(&profile_change, &event.sender().display_name()) - } - TimelineItemContent::OtherState(other_state) => { - self.update_with_other_state(&event, &other_state) - } - _ => unreachable!(), - } - - let imp = self.imp(); - imp.read_receipts.set_source(&event.read_receipts()); - imp.event.replace(Some(event)); - self.notify("event"); - } - fn update_with_other_state(&self, event: &Event, other_state: &OtherState) { let Some(room) = event.room() else { return; diff --git a/src/session/view/content/room_history/state_row/tombstone.rs b/src/session/view/content/room_history/state_row/tombstone.rs index f5892528..9c8fd073 100644 --- a/src/session/view/content/room_history/state_row/tombstone.rs +++ b/src/session/view/content/room_history/state_row/tombstone.rs @@ -6,18 +6,19 @@ use crate::{session::model::Room, spawn, toast, utils::BoundObjectWeakRef, Windo mod imp { use glib::subclass::InitializingObject; - use once_cell::sync::Lazy; use super::*; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template( resource = "/org/gnome/Fractal/ui/session/view/content/room_history/state_row/tombstone.ui" )] + #[properties(wrapper_type = super::StateTombstone)] pub struct StateTombstone { #[template_child] pub new_room_btn: TemplateChild, /// The [`Room`] this event belongs to. + #[property(get, set = Self::set_room, construct_only)] pub room: BoundObjectWeakRef, } @@ -37,41 +38,37 @@ mod imp { } } - impl ObjectImpl for StateTombstone { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![glib::ParamSpecObject::builder::("room") - .construct_only() - .build()] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - - match pspec.name() { - "room" => obj.set_room(value.get().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "room" => obj.room().to_value(), - _ => unimplemented!(), - } - } - } + #[glib::derived_properties] + impl ObjectImpl for StateTombstone {} impl WidgetImpl for StateTombstone {} impl BinImpl for StateTombstone {} + + impl StateTombstone { + /// Set the room this event belongs to. + fn set_room(&self, room: Room) { + let obj = self.obj(); + + let successor_handler = + room.connect_successor_id_string_notify(clone!(@weak self as imp => move |room| { + imp.new_room_btn.set_visible(room.successor_id().is_some()); + })); + self.new_room_btn.set_visible(room.successor_id().is_some()); + + let successor_room_handler = + room.connect_successor_notify(clone!(@weak obj => move |room| { + obj.update_button_label(room); + })); + obj.update_button_label(&room); + + self.room + .set(&room, vec![successor_handler, successor_room_handler]); + } + } } glib::wrapper! { + /// A widget presenting a room tombstone state event. pub struct StateTombstone(ObjectSubclass) @extends gtk::Widget, adw::Bin, @implements gtk::Accessible; } @@ -83,35 +80,6 @@ impl StateTombstone { glib::Object::builder().property("room", room).build() } - /// Set the room this event belongs to. - fn set_room(&self, room: Room) { - let imp = self.imp(); - - let successor_handler = room.connect_notify_local( - Some("successor"), - clone!(@weak self as obj => move |room, _| { - obj.imp().new_room_btn.set_visible(room.successor().is_some()); - }), - ); - imp.new_room_btn.set_visible(room.successor().is_some()); - - let successor_room_handler = room.connect_notify_local( - Some("successor-room"), - clone!(@weak self as obj => move |room, _| { - obj.update_button_label(room); - }), - ); - self.update_button_label(&room); - - imp.room - .set(&room, vec![successor_handler, successor_room_handler]); - } - - /// The room this event belongs to. - pub fn room(&self) -> Option { - self.imp().room.obj() - } - /// Update the button of the label. fn update_button_label(&self, room: &Room) { let button = &self.imp().new_room_btn; diff --git a/src/session/view/content/room_history/typing_row.rs b/src/session/view/content/room_history/typing_row.rs index 835144d4..689993ce 100644 --- a/src/session/view/content/room_history/typing_row.rs +++ b/src/session/view/content/room_history/typing_row.rs @@ -10,20 +10,26 @@ use crate::{ }; mod imp { + use std::marker::PhantomData; + use glib::subclass::InitializingObject; - use once_cell::sync::Lazy; use super::*; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template(resource = "/org/gnome/Fractal/ui/session/view/content/room_history/typing_row.ui")] + #[properties(wrapper_type = super::TypingRow)] pub struct TypingRow { #[template_child] pub avatar_list: TemplateChild, #[template_child] pub label: TemplateChild, /// The list of members that are currently typing. - pub bound_list: BoundObjectWeakRef, + #[property(get, set = Self::set_list, explicit_notify, nullable)] + pub list: BoundObjectWeakRef, + /// Whether the list is empty. + #[property(get = Self::is_empty, default = true)] + is_empty: PhantomData, } #[glib::object_subclass] @@ -42,43 +48,62 @@ mod imp { } } - impl ObjectImpl for TypingRow { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecObject::builder::("list") - .explicit_notify() - .build(), - glib::ParamSpecBoolean::builder("is-empty") - .default_value(true) - .read_only() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - match pspec.name() { - "list" => self.obj().set_list(value.get().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "list" => obj.list().to_value(), - "is-empty" => obj.is_empty().to_value(), - _ => unimplemented!(), - } - } - } + #[glib::derived_properties] + impl ObjectImpl for TypingRow {} impl WidgetImpl for TypingRow {} impl BinImpl for TypingRow {} + + impl TypingRow { + /// Set the list of members that are currently typing. + fn set_list(&self, list: Option) { + if self.list.obj() == list { + return; + } + let obj = self.obj(); + + let prev_is_empty = self.is_empty(); + + self.list.disconnect_signals(); + + if let Some(list) = list { + let items_changed_handler_id = list.connect_items_changed( + clone!(@weak obj => move |list, _pos, removed, added| { + if removed != 0 || added != 0 { + obj.update_label(list); + } + }), + ); + let is_empty_notify_handler_id = list + .connect_is_empty_notify(clone!(@weak obj => move |_| obj.notify_is_empty())); + + self.avatar_list.bind_model(Some(list.clone()), |item| { + item.downcast_ref::().unwrap().avatar_data().clone() + }); + + self.list.set( + &list, + vec![items_changed_handler_id, is_empty_notify_handler_id], + ); + obj.update_label(&list); + } + + if prev_is_empty != self.is_empty() { + obj.notify_is_empty(); + } + + obj.notify_list(); + } + + /// Whether the list is empty. + fn is_empty(&self) -> bool { + let Some(list) = self.list.obj() else { + return true; + }; + + list.is_empty() + } + } } glib::wrapper! { @@ -92,62 +117,6 @@ impl TypingRow { glib::Object::new() } - /// The list of members that are currently typing. - pub fn list(&self) -> Option { - self.imp().bound_list.obj() - } - - /// Set the list of members that are currently typing. - pub fn set_list(&self, list: Option<&TypingList>) { - if self.list().as_ref() == list { - return; - } - - let imp = self.imp(); - let prev_is_empty = self.is_empty(); - - imp.bound_list.disconnect_signals(); - - if let Some(list) = list { - let items_changed_handler_id = list.connect_items_changed( - clone!(@weak self as obj => move |list, _pos, removed, added| { - if removed != 0 || added != 0 { - obj.update_label(list); - } - }), - ); - let is_empty_notify_handler_id = list.connect_notify_local( - Some("is-empty"), - clone!(@weak self as obj => move |_, _| obj.notify("is-empty")), - ); - - imp.avatar_list.bind_model(Some(list.clone()), |item| { - item.downcast_ref::().unwrap().avatar_data().clone() - }); - - imp.bound_list.set( - list, - vec![items_changed_handler_id, is_empty_notify_handler_id], - ); - self.update_label(list); - } - - if prev_is_empty != self.is_empty() { - self.notify("is-empty"); - } - - self.notify("list"); - } - - /// Whether the list is empty. - pub fn is_empty(&self) -> bool { - let Some(list) = self.list() else { - return true; - }; - - list.is_empty() - } - fn update_label(&self, list: &TypingList) { let n = list.n_items(); if n == 0 { diff --git a/src/session/view/content/room_history/verification_info_bar.rs b/src/session/view/content/room_history/verification_info_bar.rs index 9e1cb59a..12d3d809 100644 --- a/src/session/view/content/room_history/verification_info_bar.rs +++ b/src/session/view/content/room_history/verification_info_bar.rs @@ -15,10 +15,11 @@ mod imp { use super::*; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template( resource = "/org/gnome/Fractal/ui/session/view/content/room_history/verification_info_bar.ui" )] + #[properties(wrapper_type = super::VerificationInfoBar)] pub struct VerificationInfoBar { #[template_child] pub revealer: TemplateChild, @@ -28,7 +29,9 @@ mod imp { pub accept_btn: TemplateChild, #[template_child] pub cancel_btn: TemplateChild, - pub request: RefCell>, + /// The identity verification presented by this info bar. + #[property(get, set = Self::set_verification, explicit_notify)] + pub verification: RefCell>, pub state_handler: RefCell>, pub user_handler: RefCell>, } @@ -50,13 +53,13 @@ mod imp { return; }; - let request = obj.request().unwrap(); - request.accept(); - window.session_view().select_item(Some(request)); + let verification = obj.verification().unwrap(); + verification.accept(); + window.session_view().select_item(Some(verification)); }); klass.install_action("verification.decline", None, move |widget, _, _| { - widget.request().unwrap().cancel(true); + widget.verification().unwrap().cancel(true); }); } @@ -65,39 +68,60 @@ mod imp { } } - impl ObjectImpl for VerificationInfoBar { - fn properties() -> &'static [glib::ParamSpec] { - use once_cell::sync::Lazy; - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecObject::builder::("request") - .explicit_notify() - .build(), - ] - }); + #[glib::derived_properties] + impl ObjectImpl for VerificationInfoBar {} - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - match pspec.name() { - "request" => self.obj().set_request(value.get().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - match pspec.name() { - "request" => self.obj().request().to_value(), - _ => unimplemented!(), - } - } - } impl WidgetImpl for VerificationInfoBar {} impl BinImpl for VerificationInfoBar {} + + impl VerificationInfoBar { + /// Set the identity verification presented by this info bar. + fn set_verification(&self, verification: Option) { + if *self.verification.borrow() == verification { + return; + } + let obj = self.obj(); + + if let Some(old_verification) = &*self.verification.borrow() { + if let Some(handler) = self.state_handler.take() { + old_verification.disconnect(handler); + } + + if let Some(handler) = self.user_handler.take() { + old_verification.user().disconnect(handler); + } + } + + if let Some(verification) = &verification { + let handler = verification.connect_notify_local( + Some("state"), + clone!(@weak obj => move |_, _| { + obj.update_view(); + }), + ); + + self.state_handler.replace(Some(handler)); + + let handler = + verification + .user() + .connect_display_name_notify(clone!(@weak obj => move |_| { + obj.update_view(); + })); + + self.user_handler.replace(Some(handler)); + } + + self.verification.replace(verification); + + obj.update_view(); + obj.notify_verification(); + } + } } glib::wrapper! { + /// An info bar presenting an ongoing identity verification. pub struct VerificationInfoBar(ObjectSubclass) @extends gtk::Widget, adw::Bin, @implements gtk::Accessible; } @@ -107,68 +131,19 @@ impl VerificationInfoBar { glib::Object::builder().property("label", &label).build() } - /// The verification request this InfoBar is showing. - pub fn request(&self) -> Option { - self.imp().request.borrow().clone() - } - - /// Set the verification request this InfoBar is showing. - pub fn set_request(&self, request: Option) { - let imp = self.imp(); - - if let Some(old_request) = &*imp.request.borrow() { - if Some(old_request) == request.as_ref() { - return; - } - - if let Some(handler) = imp.state_handler.take() { - old_request.disconnect(handler); - } - - if let Some(handler) = imp.user_handler.take() { - old_request.user().disconnect(handler); - } - } - - if let Some(ref request) = request { - let handler = request.connect_notify_local( - Some("state"), - clone!(@weak self as obj => move |_, _| { - obj.update_view(); - }), - ); - - imp.state_handler.replace(Some(handler)); - - let handler = request.user().connect_notify_local( - Some("display-name"), - clone!(@weak self as obj => move |_, _| { - obj.update_view(); - }), - ); - - imp.user_handler.replace(Some(handler)); - } - - imp.request.replace(request); - - self.update_view(); - self.notify("request"); - } - pub fn update_view(&self) { let imp = self.imp(); - let visible = if let Some(request) = self.request() { - if request.is_finished() { + let visible = if let Some(verification) = self.verification() { + if verification.is_finished() { false - } else if matches!(request.state(), VerificationState::Requested) { + } else if matches!(verification.state(), VerificationState::Requested) { imp.label.set_markup(&gettext_f( // Translators: Do NOT translate the content between '{' and '}', this is a // variable name. "{user_name} wants to be verified", &[( "user_name", - &format!("{}", request.user().display_name()), + &format!("{}", verification.user().display_name()), )], )); imp.accept_btn.set_label(&gettext("Verify"));