300 lines
9.4 KiB
Rust
300 lines
9.4 KiB
Rust
use adw::subclass::prelude::*;
|
|
use gtk::{gdk, gio, glib, glib::clone, prelude::*, CompositeTemplate};
|
|
|
|
mod read_receipts_popover;
|
|
|
|
use self::read_receipts_popover::ReadReceiptsPopover;
|
|
use super::member_timestamp::MemberTimestamp;
|
|
use crate::{
|
|
components::OverlappingAvatars,
|
|
i18n::{gettext_f, ngettext_f},
|
|
prelude::*,
|
|
session::model::{Member, MemberList, UserReadReceipt},
|
|
utils::BoundObjectWeakRef,
|
|
};
|
|
|
|
// Keep in sync with the `max-avatars` property of the `avatar_list` in the
|
|
// UI file.
|
|
const MAX_RECEIPTS_SHOWN: u32 = 10;
|
|
|
|
mod imp {
|
|
use std::cell::{Cell, RefCell};
|
|
|
|
use glib::subclass::InitializingObject;
|
|
|
|
use super::*;
|
|
|
|
#[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>,
|
|
#[template_child]
|
|
pub avatar_list: TemplateChild<OverlappingAvatars>,
|
|
/// 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>,
|
|
/// The displayed member if there is only one receipt.
|
|
pub receipt_member: BoundObjectWeakRef<Member>,
|
|
}
|
|
|
|
impl Default for ReadReceiptsList {
|
|
fn default() -> Self {
|
|
Self {
|
|
label: Default::default(),
|
|
avatar_list: Default::default(),
|
|
active: Default::default(),
|
|
members: Default::default(),
|
|
list: gio::ListStore::new::<MemberTimestamp>(),
|
|
source: Default::default(),
|
|
receipt_member: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[glib::object_subclass]
|
|
impl ObjectSubclass for ReadReceiptsList {
|
|
const NAME: &'static str = "ContentReadReceiptsList";
|
|
type Type = super::ReadReceiptsList;
|
|
type ParentType = adw::Bin;
|
|
|
|
fn class_init(klass: &mut Self::Class) {
|
|
Self::bind_template(klass);
|
|
Self::Type::bind_template_callbacks(klass);
|
|
|
|
klass.set_css_name("read-receipts-list");
|
|
klass.set_accessible_role(gtk::AccessibleRole::ToggleButton);
|
|
}
|
|
|
|
fn instance_init(obj: &InitializingObject<Self>) {
|
|
obj.init_template();
|
|
}
|
|
}
|
|
|
|
#[glib::derived_properties]
|
|
impl ObjectImpl for ReadReceiptsList {
|
|
fn constructed(&self) {
|
|
self.parent_constructed();
|
|
let obj = self.obj();
|
|
|
|
self.avatar_list
|
|
.bind_model(Some(self.list.clone()), |item| {
|
|
item.downcast_ref::<MemberTimestamp>()
|
|
.and_then(|m| m.member())
|
|
.map(|m| m.avatar_data().clone())
|
|
.unwrap()
|
|
});
|
|
|
|
self.list
|
|
.connect_items_changed(clone!(@weak obj => move |_, _,_,_| {
|
|
obj.update_tooltip();
|
|
obj.update_label();
|
|
}));
|
|
|
|
obj.set_pressed_state(false);
|
|
}
|
|
}
|
|
|
|
impl WidgetImpl for ReadReceiptsList {}
|
|
impl BinImpl for ReadReceiptsList {}
|
|
|
|
impl AccessibleImpl for ReadReceiptsList {
|
|
fn first_accessible_child(&self) -> Option<gtk::Accessible> {
|
|
// Hide the children in the a11y tree.
|
|
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! {
|
|
/// A widget displaying the read receipts on a message.
|
|
pub struct ReadReceiptsList(ObjectSubclass<imp::ReadReceiptsList>)
|
|
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
|
|
}
|
|
|
|
#[gtk::template_callbacks]
|
|
impl ReadReceiptsList {
|
|
pub fn new(members: &MemberList) -> Self {
|
|
glib::Object::builder().property("members", members).build()
|
|
}
|
|
|
|
/// Set whether this list is active.
|
|
fn set_active(&self, active: bool) {
|
|
if self.active() == active {
|
|
return;
|
|
}
|
|
|
|
self.imp().active.set(active);
|
|
self.notify_active();
|
|
self.set_pressed_state(active);
|
|
}
|
|
|
|
/// Set the CSS and a11 states.
|
|
fn set_pressed_state(&self, pressed: bool) {
|
|
if pressed {
|
|
self.set_state_flags(gtk::StateFlags::CHECKED, false);
|
|
} else {
|
|
self.unset_state_flags(gtk::StateFlags::CHECKED);
|
|
}
|
|
|
|
let tristate = if pressed {
|
|
gtk::AccessibleTristate::True
|
|
} else {
|
|
gtk::AccessibleTristate::False
|
|
};
|
|
self.update_state(&[gtk::accessible::State::Pressed(tristate)]);
|
|
}
|
|
|
|
/// Set the read receipts that are used as a source of data.
|
|
pub fn set_source(&self, source: &gio::ListStore) {
|
|
let imp = self.imp();
|
|
|
|
let items_changed_handler_id = source.connect_items_changed(
|
|
clone!(@weak self as obj => move |source, pos, removed, added| {
|
|
obj.items_changed(source, pos, removed, added);
|
|
}),
|
|
);
|
|
self.items_changed(source, 0, self.list().n_items(), source.n_items());
|
|
|
|
imp.source.set(source, vec![items_changed_handler_id]);
|
|
}
|
|
|
|
fn items_changed(&self, source: &gio::ListStore, pos: u32, removed: u32, added: u32) {
|
|
let Some(members) = &*self.imp().members.borrow() else {
|
|
return;
|
|
};
|
|
|
|
let mut new_receipts = Vec::with_capacity(added as usize);
|
|
|
|
for i in pos..pos + added {
|
|
let Some(boxed) = source.item(i).and_downcast::<glib::BoxedAnyObject>() else {
|
|
break;
|
|
};
|
|
|
|
let source_receipt = boxed.borrow::<UserReadReceipt>();
|
|
let member = members.get_or_create(source_receipt.user_id.clone());
|
|
let receipt = MemberTimestamp::new(
|
|
&member,
|
|
source_receipt.receipt.ts.map(|ts| ts.as_secs().into()),
|
|
);
|
|
|
|
new_receipts.push(receipt);
|
|
}
|
|
|
|
self.list().splice(pos, removed, &new_receipts);
|
|
}
|
|
|
|
fn update_tooltip(&self) {
|
|
let imp = self.imp();
|
|
imp.receipt_member.disconnect_signals();
|
|
let n_items = self.list().n_items();
|
|
|
|
if n_items == 1 {
|
|
if let Some(member) = self
|
|
.list()
|
|
.item(0)
|
|
.and_downcast::<MemberTimestamp>()
|
|
.and_then(|r| r.member())
|
|
{
|
|
// Listen to changes of the display name.
|
|
let handler_id = member.connect_notify_local(
|
|
Some("display-name"),
|
|
clone!(@weak self as obj => move |member, _| {
|
|
obj.update_member_tooltip(member);
|
|
}),
|
|
);
|
|
|
|
imp.receipt_member.set(&member, vec![handler_id]);
|
|
self.update_member_tooltip(&member);
|
|
return;
|
|
}
|
|
}
|
|
|
|
let text = (n_items > 0).then(|| {
|
|
ngettext_f(
|
|
// Translators: Do NOT translate the content between '{' and '}', this is a
|
|
// variable name.
|
|
"Seen by 1 member",
|
|
"Seen by {n} members",
|
|
n_items,
|
|
&[("n", &n_items.to_string())],
|
|
)
|
|
});
|
|
|
|
self.set_tooltip_text(text.as_deref())
|
|
}
|
|
|
|
fn update_member_tooltip(&self, member: &Member) {
|
|
// Translators: Do NOT translate the content between '{' and '}', this is a
|
|
// variable name.
|
|
let text = gettext_f("Seen by {name}", &[("name", &member.display_name())]);
|
|
|
|
self.set_tooltip_text(Some(&text));
|
|
}
|
|
|
|
fn update_label(&self) {
|
|
let label = &self.imp().label;
|
|
let n_items = self.list().n_items();
|
|
|
|
if n_items > MAX_RECEIPTS_SHOWN {
|
|
label.set_text(&format!("{} +", n_items - MAX_RECEIPTS_SHOWN));
|
|
label.set_visible(true);
|
|
} else {
|
|
label.set_visible(false);
|
|
}
|
|
}
|
|
|
|
/// Handle a click on the container.
|
|
///
|
|
/// 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) {
|
|
let list = self.list();
|
|
if list.n_items() == 0 {
|
|
// No popover.
|
|
return;
|
|
}
|
|
self.set_active(true);
|
|
|
|
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| {
|
|
popover.unparent();
|
|
obj.set_active(false);
|
|
}));
|
|
|
|
popover.popup();
|
|
}
|
|
}
|