room-history: Port to glib::Properties macro

This commit is contained in:
Kévin Commaille 2023-12-19 19:21:07 +01:00
parent db339a476a
commit 118f4ca1b0
No known key found for this signature in database
GPG Key ID: 29A48C1F03620416
30 changed files with 1043 additions and 1940 deletions

View File

@ -125,6 +125,10 @@ mod imp {
pub predecessor_id: OnceCell<OwnedRoomId>,
/// The ID of the successor of this Room, if this room was upgraded.
pub successor_id: OnceCell<OwnedRoomId>,
/// 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<Option<String>>,
/// 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<String> {
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 {

View File

@ -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<gtk::Label>,
pub inner_label: TemplateChild<gtk::Label>,
/// The label of this divider.
#[property(get = Self::label, set = Self::set_label)]
label: PhantomData<String>,
}
#[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<Vec<glib::ParamSpec>> = 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<imp::DividerRow>)
@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()
}
}

View File

@ -17,7 +17,7 @@
</object>
</child>
<child>
<object class="GtkLabel" id="label">
<object class="GtkLabel" id="inner_label">
<style>
<class name="dim-label"/>
</style>

View File

@ -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<RoomHistory>,
pub message_toolbar_handler: RefCell<Option<glib::SignalHandlerId>>,
/// The [`TimelineItem`] presented by this row.
#[property(get, set = Self::set_item, explicit_notify, nullable)]
pub item: RefCell<Option<TimelineItem>>,
pub action_group: RefCell<Option<gio::SimpleActionGroup>>,
pub notify_handlers: RefCell<Vec<glib::SignalHandlerId>>,
@ -47,40 +52,8 @@ mod imp {
}
}
#[glib::derived_properties]
impl ObjectImpl for ItemRow {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<TimelineItem>("item").build(),
glib::ParamSpecObject::builder::<RoomHistory>("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<TimelineItem>) {
let obj = self.obj();
// Reinitialize the header.
obj.remove_css_class("has-header");
if let Some(event) = self.item.borrow().and_downcast_ref::<Event>() {
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::<Event>() {
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::<VirtualItem>() {
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::<Spinner>()) {
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::<TypingRow>()
{
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::<DividerRow>() {
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::<DividerRow>() {
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: <https://docs.gtk.org/glib/method.DateTime.format.html>
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: <https://docs.gtk.org/glib/method.DateTime.format.html>
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::<DividerRow>() {
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<imp::ItemRow>)
@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<gio::SimpleActionGroup> {
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<TimelineItem> {
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<TimelineItem>) {
let imp = self.imp();
// Reinitialize the header.
self.remove_css_class("has-header");
if let Some(event) = imp.item.borrow().and_downcast_ref::<Event>() {
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::<Event>() {
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::<VirtualItem>() {
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::<Spinner>()) {
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::<TypingRow>() {
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::<DividerRow>() {
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::<DividerRow>() {
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: <https://docs.gtk.org/glib/method.DateTime.format.html>
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: <https://docs.gtk.org/glib/method.DateTime.format.html>
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::<DividerRow>() {
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(_)

View File

@ -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<Member>,
/// The timestamp, in seconds since Unix Epoch.
///
/// A value of 0 means no timestamp.
#[property(get, construct_only)]
pub timestamp: Cell<u64>,
}
@ -28,42 +29,8 @@ mod imp {
type Type = super::MemberTimestamp;
}
impl ObjectImpl for MemberTimestamp {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<Member>("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::<Option<Member>>().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<Member> {
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");
}
}

View File

@ -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<gtk::Label>,
/// The `MemberTimestamp` presented by this row.
#[property(get, set = Self::set_data, explicit_notify, nullable)]
pub data: glib::WeakRef<MemberTimestamp>,
pub system_settings_handler: RefCell<Option<glib::SignalHandlerId>>,
}
@ -40,35 +41,8 @@ mod imp {
}
}
#[glib::derived_properties]
impl ObjectImpl for MemberTimestampRow {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecObject::builder::<MemberTimestamp>("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::<Option<MemberTimestamp>>().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<MemberTimestamp>) {
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<MemberTimestamp> {
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();

View File

@ -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<Option<String>>,
/// The state of the audio file.
#[property(get, builder(MediaState::default()))]
pub state: Cell<MediaState>,
/// Whether to display this audio message in a compact format.
#[property(get)]
pub compact: Cell<bool>,
#[template_child]
pub player: TemplateChild<AudioPlayer>,
@ -57,44 +60,10 @@ mod imp {
}
}
impl ObjectImpl for MessageAudio {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecString::builder("body").read_only().build(),
glib::ParamSpecEnum::builder::<MediaState>("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<String> {
self.imp().body.borrow().to_owned()
}
/// Set the body of the audio message.
fn set_body(&self, body: Option<String>) {
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

View File

@ -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<ContentFormat>,
}
@ -55,37 +56,27 @@ mod imp {
type ParentType = adw::Bin;
}
impl ObjectImpl for MessageContent {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecEnum::builder::<ContentFormat>("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<imp::MessageContent>)
@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

View File

@ -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<Option<String>>,
/// Whether this file should be displayed in a compact format.
#[property(get, set = Self::set_compact, explicit_notify)]
pub compact: Cell<bool>,
}
@ -37,50 +39,39 @@ mod imp {
}
}
impl ObjectImpl for MessageFile {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = 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<String>) {
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<imp::MessageFile>)
@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<String>) {
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<String> {
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,

View File

@ -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<i32>,
/// The intended display height of the media.
#[property(get, set = Self::set_height, explicit_notify, default = -1, minimum = -1)]
pub height: Cell<i32>,
/// The state of the media.
#[property(get, builder(MediaState::default()))]
pub state: Cell<MediaState>,
/// Whether to display this media in a compact format.
#[property(get)]
pub compact: Cell<bool>,
#[template_child]
pub media: TemplateChild<gtk::Overlay>,
@ -97,61 +101,8 @@ mod imp {
}
}
#[glib::derived_properties]
impl ObjectImpl for MessageMedia {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = 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::<MediaState>("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<imp::MessageMedia>)
@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.

View File

@ -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<MessageState>,
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
@ -40,39 +41,85 @@ mod imp {
}
}
impl ObjectImpl for MessageStateStack {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecEnum::builder::<MessageState>("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");
}
}

View File

@ -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<Avatar>,
@ -55,7 +55,14 @@ mod imp {
pub read_receipts: TemplateChild<ReadReceiptsList>,
pub bindings: RefCell<Vec<glib::Binding>>,
pub system_settings_handler: RefCell<Option<glib::SignalHandlerId>>,
/// The event that is presented.
#[property(get, set = Self::set_event, explicit_notify)]
pub event: BoundObject<Event>,
/// Whether this item should show its header.
///
/// This is ignored if this event doesnt have a header.
#[property(get = Self::show_header, set = Self::set_show_header, explicit_notify)]
pub show_header: PhantomData<bool>,
}
#[glib::object_subclass]
@ -77,53 +84,19 @@ mod imp {
}
}
#[glib::derived_properties]
impl ObjectImpl for MessageRow {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecBoolean::builder("show-header")
.explicit_notify()
.build(),
glib::ParamSpecObject::builder::<Event>("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 doesnt 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<imp::MessageRow>)
@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 doesnt 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<Event> {
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) {

View File

@ -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<ReactionGroup>,
/// 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<Option<MemberList>>,
#[template_child]
pub button: TemplateChild<gtk::ToggleButton>,
@ -69,50 +72,63 @@ mod imp {
}
}
impl ObjectImpl for MessageReaction {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<ReactionGroup>("group")
.construct_only()
.build(),
glib::ParamSpecObject::builder::<gio::ListStore>("list")
.read_only()
.build(),
glib::ParamSpecObject::builder::<MemberList>("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<MemberList>) {
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<imp::MessageReaction>)
@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<ReactionGroup> {
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<MemberList> {
self.imp().members.borrow().clone()
}
/// Set the members list of the room of the reaction.
pub fn set_members(&self, members: Option<MemberList>) {
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();

View File

@ -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<gtk::ListView>,
/// The reaction senders to display.
#[property(get, set = Self::set_senders, construct_only)]
pub senders: glib::WeakRef<gio::ListStore>,
}
@ -36,43 +37,20 @@ mod imp {
}
}
impl ObjectImpl for ReactionPopover {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecObject::builder::<gio::ListStore>("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(&gtk::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<gio::ListStore> {
self.imp().senders.upgrade()
}
/// Set the reaction senders to display.
fn set_senders(&self, senders: Option<gio::ListStore>) {
let Some(senders) = senders else {
// Ignore missing reaction senders.
return;
};
let imp = self.imp();
imp.senders.set(Some(&senders));
imp.list
.set_model(Some(&gtk::NoSelection::new(Some(senders))));
self.notify("senders");
}
}

View File

@ -35,9 +35,7 @@ mod imp {
}
impl ObjectImpl for MessageReactionList {}
impl WidgetImpl for MessageReactionList {}
impl BinImpl for MessageReactionList {}
}

View File

@ -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<imp::MessageReply>)
@extends gtk::Widget, gtk::Grid, @implements gtk::Accessible;
}

View File

@ -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<String>,
/// Whether the original text is HTML.
///
/// Only used for emotes.
#[property(get)]
pub is_html: Cell<bool>,
/// The text format.
#[property(get, builder(ContentFormat::default()))]
pub format: Cell<ContentFormat>,
/// The sender of the message, if we need to listen to changes.
pub sender: BoundObjectWeakRef<Member>,
@ -49,39 +51,10 @@ mod imp {
type ParentType = adw::Bin;
}
impl ObjectImpl for MessageText {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecString::builder("original-text")
.read_only()
.build(),
glib::ParamSpecBoolean::builder("is-html")
.read_only()
.build(),
glib::ParamSpecEnum::builder::<ContentFormat>("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.

View File

@ -63,6 +63,7 @@ mod imp {
}
glib::wrapper! {
/// A dialog to preview an attachment before sending it.
pub struct AttachmentDialog(ObjectSubclass<imp::AttachmentDialog>)
@extends gtk::Widget, gtk::Window, gtk::Root, adw::Window;
}

View File

@ -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<gtk::ListBox>,
/// The parent `GtkTextView` to autocomplete.
#[property(get = Self::view)]
view: PhantomData<gtk::TextView>,
/// The user ID of the current session.
#[property(get, set = Self::set_user_id, explicit_notify, nullable)]
pub user_id: RefCell<Option<String>>,
/// 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<Option<MemberList>>,
/// 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<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<gtk::TextView>("view")
.read_only()
.build(),
glib::ParamSpecString::builder("user-id")
.explicit_notify()
.build(),
glib::ParamSpecObject::builder::<MemberList>("members")
.explicit_notify()
.build(),
glib::ParamSpecObject::builder::<gtk::FilterListModel>("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::<gtk::TextView>().unwrap()
}
/// Set the ID of the logged-in user.
fn set_user_id(&self, user_id: Option<String>) {
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<MemberList> {
self.members_expr.model().and_downcast()
}
/// Set the room members used for completion.
fn set_members(&self, members: Option<MemberList>) {
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::<gtk::TextView>().unwrap()
}
/// The ID of the logged-in user.
pub fn user_id(&self) -> Option<String> {
self.imp().user_id.borrow().clone()
}
/// Set the ID of the logged-in user.
pub fn set_user_id(&self, user_id: Option<String>) {
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<MemberList> {
self.imp().members_expr.model().and_downcast()
}
/// Set the room members used for completion.
pub fn set_members(&self, members: Option<MemberList>) {
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) -> &gtk::FilterListModel {
&self.imp().filtered_members
}
fn current_word(&self) -> Option<(gtk::TextIter, gtk::TextIter, String)> {
self.imp().current_word.borrow().clone()
}

View File

@ -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<Avatar>,
@ -26,6 +26,7 @@ mod imp {
#[template_child]
pub id: TemplateChild<gtk::Label>,
/// The room member presented by this row.
#[property(get, set = Self::set_member, explicit_notify, nullable)]
pub member: RefCell<Option<Member>>,
}
@ -44,34 +45,33 @@ mod imp {
}
}
impl ObjectImpl for CompletionRow {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecObject::builder::<Member>("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<Member>) {
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::<AvatarData>);
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<Member> {
self.imp().member.borrow().clone()
}
/// Set the room member displayed by this row.
pub fn set_member(&self, member: Option<Member>) {
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::<AvatarData>::None);
imp.display_name.set_label("");
imp.id.set_label("");
}
imp.member.replace(member);
self.notify("member");
}
}
impl Default for CompletionRow {

View File

@ -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<Room>,
/// Whether our own user can send messages in the current room.
#[property(get)]
pub can_send_messages: Cell<bool>,
pub own_member: glib::WeakRef<Member>,
pub power_levels_handler: RefCell<Option<glib::SignalHandlerId>>,
pub md_enabled: Cell<bool>,
/// Whether outgoing messages should be interpreted as markdown.
#[property(get, set)]
pub markdown_enabled: Cell<bool>,
pub completion: CompletionPopover,
#[template_child]
pub message_entry: TemplateChild<sourceview::View>,
@ -86,7 +92,11 @@ mod imp {
pub related_event_header: TemplateChild<LabelWithWidgets>,
#[template_child]
pub related_event_content: TemplateChild<MessageContent>,
/// The type of related event of the composer.
#[property(get, builder(RelatedEventType::default()))]
pub related_event_type: Cell<RelatedEventType>,
/// The related event of the composer.
#[property(get)]
pub related_event: RefCell<Option<Event>>,
}
@ -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<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<Room>("room")
.explicit_notify()
.build(),
glib::ParamSpecBoolean::builder("can-send-messages")
.read_only()
.build(),
glib::ParamSpecBoolean::builder("markdown-enabled")
.explicit_notify()
.build(),
glib::ParamSpecEnum::builder::<RelatedEventType>("related-event-type")
.read_only()
.build(),
glib::ParamSpecObject::builder::<Event>("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::<Option<Room>>().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<Room>) {
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<Room> {
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<Member> {
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<Event> {
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();
}),
);

View File

@ -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<Option<Room>>,
/// Whether this is the only view visible, i.e. there is no sidebar.
#[property(get, set)]
pub only_view: Cell<bool>,
/// Whether this `RoomHistory` is empty, aka no room is currently
/// displayed.
#[property(get = Self::empty)]
empty: PhantomData<bool>,
pub room_members: RefCell<Option<MemberList>>,
pub room_handlers: RefCell<Vec<SignalHandlerId>>,
pub timeline_handlers: RefCell<Vec<SignalHandlerId>>,
pub is_auto_scrolling: Cell<bool>,
/// Whether the room history should stick to the newest message in the
/// timeline.
#[property(get, set = Self::set_sticky, explicit_notify)]
pub sticky: Cell<bool>,
pub item_context_menu: OnceCell<gtk::PopoverMenu>,
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<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<Room>("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<Room>) {
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<imp::RoomHistory>)
@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<Room>) {
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<Room> {
self.imp().room.borrow().clone()
}
/// The members of the room currently displayed.
pub fn room_members(&self) -> Option<MemberList> {
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) -> &gtk::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();

View File

@ -72,7 +72,7 @@
</child>
<child>
<object class="ContentVerificationInfoBar" id="verification_info_bar">
<binding name="request">
<binding name="verification">
<lookup name="verification">
<lookup name="room">ContentRoomHistory</lookup>
</lookup>

View File

@ -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<gtk::Label>,
@ -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<bool>,
/// The list of room members.
#[property(get, set = Self::set_members, explicit_notify, nullable)]
pub members: RefCell<Option<MemberList>>,
/// The list of read receipts.
#[property(get)]
pub list: gio::ListStore,
/// The read receipts used as a source.
pub source: BoundObjectWeakRef<gio::ListStore>,
@ -81,43 +84,8 @@ mod imp {
}
}
#[glib::derived_properties]
impl ObjectImpl for ReadReceiptsList {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecBoolean::builder("active")
.read_only()
.build(),
glib::ParamSpecObject::builder::<MemberList>("members").build(),
glib::ParamSpecObject::builder::<gio::ListStore>("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<MemberList>) {
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<MemberList> {
self.imp().members.borrow().clone()
}
/// Set the list of room members.
pub fn set_members(&self, members: Option<MemberList>) {
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| {

View File

@ -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<gtk::ListView>,
/// The receipts to display.
#[property(get, set = Self::set_receipts, construct_only)]
pub receipts: glib::WeakRef<gio::ListStore>,
}
@ -35,35 +36,8 @@ mod imp {
}
}
#[glib::derived_properties]
impl ObjectImpl for ReadReceiptsPopover {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecObject::builder::<gio::ListStore>("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(&gtk::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<gio::ListStore> {
self.imp().receipts.upgrade()
}
/// Set the receipts to display.
fn set_receipts(&self, receipts: Option<gio::ListStore>) {
let Some(receipts) = receipts else {
// Ignore missing receipts.
return;
};
let imp = self.imp();
imp.receipts.set(Some(&receipts));
imp.list
.set_model(Some(&gtk::NoSelection::new(Some(receipts))));
self.notify("receipts");
}
}

View File

@ -41,6 +41,7 @@ mod imp {
}
glib::wrapper! {
/// A widget presenting a room create state event.
pub struct StateCreation(ObjectSubclass<imp::StateCreation>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}

View File

@ -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<adw::Bin>,
#[template_child]
pub read_receipts: TemplateChild<ReadReceiptsList>,
/// The state event displayed by this widget.
#[property(get, set = Self::set_event)]
pub event: RefCell<Option<Event>>,
}
@ -56,39 +57,38 @@ mod imp {
}
}
impl ObjectImpl for StateRow {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecObject::builder::<Event>("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<imp::StateRow>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
@ -102,30 +102,6 @@ impl StateRow {
&self.imp().content
}
pub fn event(&self) -> Option<Event> {
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;

View File

@ -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<gtk::Button>,
/// The [`Room`] this event belongs to.
#[property(get, set = Self::set_room, construct_only)]
pub room: BoundObjectWeakRef<Room>,
}
@ -37,41 +38,37 @@ mod imp {
}
}
impl ObjectImpl for StateTombstone {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecObject::builder::<Room>("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<imp::StateTombstone>)
@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<Room> {
self.imp().room.obj()
}
/// Update the button of the label.
fn update_button_label(&self, room: &Room) {
let button = &self.imp().new_room_btn;

View File

@ -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<OverlappingAvatars>,
#[template_child]
pub label: TemplateChild<gtk::Label>,
/// The list of members that are currently typing.
pub bound_list: BoundObjectWeakRef<TypingList>,
#[property(get, set = Self::set_list, explicit_notify, nullable)]
pub list: BoundObjectWeakRef<TypingList>,
/// Whether the list is empty.
#[property(get = Self::is_empty, default = true)]
is_empty: PhantomData<bool>,
}
#[glib::object_subclass]
@ -42,43 +48,62 @@ mod imp {
}
}
impl ObjectImpl for TypingRow {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<TypingList>("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<TypingList>) {
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::<Member>().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<TypingList> {
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::<Member>().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 {

View File

@ -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<gtk::Revealer>,
@ -28,7 +29,9 @@ mod imp {
pub accept_btn: TemplateChild<gtk::Button>,
#[template_child]
pub cancel_btn: TemplateChild<gtk::Button>,
pub request: RefCell<Option<IdentityVerification>>,
/// The identity verification presented by this info bar.
#[property(get, set = Self::set_verification, explicit_notify)]
pub verification: RefCell<Option<IdentityVerification>>,
pub state_handler: RefCell<Option<SignalHandlerId>>,
pub user_handler: RefCell<Option<SignalHandlerId>>,
}
@ -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<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<IdentityVerification>("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<IdentityVerification>) {
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<imp::VerificationInfoBar>)
@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<IdentityVerification> {
self.imp().request.borrow().clone()
}
/// Set the verification request this InfoBar is showing.
pub fn set_request(&self, request: Option<IdentityVerification>) {
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!("<b>{}</b>", request.user().display_name()),
&format!("<b>{}</b>", verification.user().display_name()),
)],
));
imp.accept_btn.set_label(&gettext("Verify"));