room-history: Port to glib::Properties macro
This commit is contained in:
parent
db339a476a
commit
118f4ca1b0
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label">
|
||||
<object class="GtkLabel" id="inner_label">
|
||||
<style>
|
||||
<class name="dim-label"/>
|
||||
</style>
|
||||
|
|
|
@ -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(_)
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 doesn’t 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 doesn’t have a header.
|
||||
fn show_header(&self) -> bool {
|
||||
self.avatar.is_visible() && self.header.is_visible()
|
||||
}
|
||||
|
||||
/// Set whether this item should show its header.
|
||||
fn set_show_header(&self, visible: bool) {
|
||||
let obj = self.obj();
|
||||
|
||||
self.avatar.set_visible(visible);
|
||||
self.header.set_visible(visible);
|
||||
|
||||
if let Some(row) = obj.parent() {
|
||||
if visible {
|
||||
row.add_css_class("has-header");
|
||||
} else {
|
||||
row.remove_css_class("has-header");
|
||||
}
|
||||
}
|
||||
|
||||
obj.notify_show_header();
|
||||
}
|
||||
|
||||
/// Set the event that is presented.
|
||||
fn set_event(&self, event: Event) {
|
||||
let Some(room) = event.room() else {
|
||||
return;
|
||||
};
|
||||
let obj = self.obj();
|
||||
|
||||
// Remove signals and bindings from the previous event.
|
||||
self.event.disconnect_signals();
|
||||
while let Some(binding) = self.bindings.borrow_mut().pop() {
|
||||
binding.unbind();
|
||||
}
|
||||
|
||||
self.avatar
|
||||
.set_data(Some(event.sender().avatar_data().clone()));
|
||||
|
||||
let display_name_binding = event
|
||||
.sender()
|
||||
.bind_property("display-name", &*self.display_name, "label")
|
||||
.sync_create()
|
||||
.build();
|
||||
|
||||
let show_header_binding = event
|
||||
.bind_property("show-header", &*obj, "show-header")
|
||||
.sync_create()
|
||||
.build();
|
||||
|
||||
let state_binding = event
|
||||
.bind_property("state", &*self.message_state, "state")
|
||||
.sync_create()
|
||||
.build();
|
||||
|
||||
self.bindings.borrow_mut().append(&mut vec![
|
||||
display_name_binding,
|
||||
show_header_binding,
|
||||
state_binding,
|
||||
]);
|
||||
|
||||
let timestamp_handler = event.connect_timestamp_notify(clone!(@weak obj => move |_| {
|
||||
obj.update_timestamp();
|
||||
}));
|
||||
|
||||
let source_handler = event.connect_source_notify(clone!(@weak obj => move |_| {
|
||||
obj.update_content();
|
||||
}));
|
||||
|
||||
self.reactions
|
||||
.set_reaction_list(&room.get_or_create_members(), &event.reactions());
|
||||
self.read_receipts.set_source(&event.read_receipts());
|
||||
self.event
|
||||
.set(event, vec![timestamp_handler, source_handler]);
|
||||
obj.notify_event();
|
||||
|
||||
obj.update_content();
|
||||
obj.update_timestamp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// A row displaying a message in the timeline.
|
||||
pub struct MessageRow(ObjectSubclass<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 doesn’t have a header.
|
||||
pub fn show_header(&self) -> bool {
|
||||
let imp = self.imp();
|
||||
imp.avatar.is_visible() && imp.header.is_visible()
|
||||
}
|
||||
|
||||
/// Set whether this item should show its header.
|
||||
pub fn set_show_header(&self, visible: bool) {
|
||||
let imp = self.imp();
|
||||
imp.avatar.set_visible(visible);
|
||||
imp.header.set_visible(visible);
|
||||
|
||||
if let Some(row) = self.parent() {
|
||||
if visible {
|
||||
row.add_css_class("has-header");
|
||||
} else {
|
||||
row.remove_css_class("has-header");
|
||||
}
|
||||
}
|
||||
|
||||
self.notify("show-header");
|
||||
}
|
||||
|
||||
pub fn set_content_format(&self, format: ContentFormat) {
|
||||
self.imp().content.set_format(format);
|
||||
}
|
||||
|
||||
pub fn event(&self) -> Option<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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(>k::NoSelection::new(Some(senders))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
|
@ -86,23 +64,4 @@ impl ReactionPopover {
|
|||
pub fn new(senders: &gio::ListStore) -> Self {
|
||||
glib::Object::builder().property("senders", senders).build()
|
||||
}
|
||||
|
||||
/// The reaction senders to display.
|
||||
pub fn senders(&self) -> Option<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(>k::NoSelection::new(Some(senders))));
|
||||
self.notify("senders");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,9 +35,7 @@ mod imp {
|
|||
}
|
||||
|
||||
impl ObjectImpl for MessageReactionList {}
|
||||
|
||||
impl WidgetImpl for MessageReactionList {}
|
||||
|
||||
impl BinImpl for MessageReactionList {}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) -> >k::FilterListModel {
|
||||
&self.imp().filtered_members
|
||||
}
|
||||
|
||||
fn current_word(&self) -> Option<(gtk::TextIter, gtk::TextIter, String)> {
|
||||
self.imp().current_word.borrow().clone()
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -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) -> >k::NoSelection {
|
||||
self.imp()
|
||||
.selection_model
|
||||
|
@ -699,30 +669,6 @@ impl RoomHistory {
|
|||
self.root().and_downcast()
|
||||
}
|
||||
|
||||
/// Whether the room history should stick to the newest message in the
|
||||
/// timeline.
|
||||
pub fn sticky(&self) -> bool {
|
||||
self.imp().sticky.get()
|
||||
}
|
||||
|
||||
/// Set whether the room history should stick to the newest message in the
|
||||
/// timeline.
|
||||
pub fn set_sticky(&self, sticky: bool) {
|
||||
let imp = self.imp();
|
||||
|
||||
if self.sticky() == sticky {
|
||||
return;
|
||||
}
|
||||
|
||||
if !sticky {
|
||||
imp.scroll_btn_revealer.set_visible(true);
|
||||
}
|
||||
imp.scroll_btn_revealer.set_reveal_child(!sticky);
|
||||
|
||||
imp.sticky.set(sticky);
|
||||
self.notify("sticky");
|
||||
}
|
||||
|
||||
/// Scroll to the newest message in the timeline
|
||||
pub fn scroll_down(&self) {
|
||||
let imp = self.imp();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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(>k::NoSelection::new(Some(receipts))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
|
@ -86,23 +69,4 @@ impl ReadReceiptsPopover {
|
|||
.property("receipts", receipts)
|
||||
.build()
|
||||
}
|
||||
|
||||
/// The receipts to display.
|
||||
pub fn receipts(&self) -> Option<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(>k::NoSelection::new(Some(receipts))));
|
||||
self.notify("receipts");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"));
|
||||
|
|
Loading…
Reference in New Issue