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::{add_activate_binding_action, BoundObjectWeakRef},
};
const MAX_RECEIPTS_SHOWN: u32 = 5;
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 content: TemplateChild<gtk::Box>,
#[template_child]
pub label: TemplateChild<gtk::Label>,
#[template_child]
pub avatar_list: TemplateChild<OverlappingAvatars>,
#[property(get)]
pub active: Cell<bool>,
#[property(get, set = Self::set_members, explicit_notify, nullable)]
pub members: RefCell<Option<MemberList>>,
#[property(get)]
pub list: gio::ListStore,
pub source: BoundObjectWeakRef<gio::ListStore>,
pub receipt_member: BoundObjectWeakRef<Member>,
}
impl Default for ReadReceiptsList {
fn default() -> Self {
Self {
content: Default::default(),
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 = gtk::Widget;
fn class_init(klass: &mut Self::Class) {
klass.set_layout_manager_type::<gtk::BinLayout>();
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
klass.set_css_name("read-receipts-list");
klass.set_accessible_role(gtk::AccessibleRole::ToggleButton);
klass.install_action("read-receipts-list.activate", None, |obj, _, _| {
obj.show_popover(1, 0.0, 0.0);
});
add_activate_binding_action(klass, "read-receipts-list.activate");
}
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);
}
fn dispose(&self) {
self.content.unparent();
}
}
impl WidgetImpl for ReadReceiptsList {}
impl AccessibleImpl for ReadReceiptsList {
fn first_accessible_child(&self) -> Option<gtk::Accessible> {
None
}
}
impl ReadReceiptsList {
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! {
pub struct ReadReceiptsList(ObjectSubclass<imp::ReadReceiptsList>)
@extends gtk::Widget, @implements gtk::Accessible;
}
#[gtk::template_callbacks]
impl ReadReceiptsList {
pub fn new(members: &MemberList) -> Self {
glib::Object::builder().property("members", members).build()
}
fn set_active(&self, active: bool) {
if self.active() == active {
return;
}
self.imp().active.set(active);
self.notify_active();
self.set_pressed_state(active);
}
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)]);
}
pub fn set_source(&self, source: &gio::ListStore) {
let imp = self.imp();
let items_changed_handler_id = source.connect_items_changed(clone!(
#[weak(rename_to = obj)]
self,
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())
{
let handler_id = member.connect_display_name_notify(clone!(
#[weak(rename_to = obj)]
self,
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(
"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) {
let text = gettext_f("Seen by {name}", &[("name", &member.disambiguated_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);
}
}
#[template_callback]
fn show_popover(&self, _n_press: i32, x: f64, y: f64) {
let list = self.list();
if list.n_items() == 0 {
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(rename_to = obj)]
self,
move |popover| {
popover.unparent();
obj.set_active(false);
}
));
popover.popup();
}
}