room-history: Add popover to show senders of a reaction
The popover will show for a reaction the list of people that have sent it and when they did. * Add a `ReactionPopover` object for the popover * Add a `ReactionSenderRow` object for senders as rows of the popover * Add a `MemberReactionSender` object to represent a room member that sent a reaction
This commit is contained in:
parent
7bb3790d7a
commit
cd1911fce8
12 changed files with 744 additions and 125 deletions
|
@ -85,6 +85,7 @@ src/session/view/content/room_history/message_row/file.ui
|
|||
src/session/view/content/room_history/message_row/location.rs
|
||||
src/session/view/content/room_history/message_row/media.rs
|
||||
src/session/view/content/room_history/message_row/mod.ui
|
||||
src/session/view/content/room_history/message_row/reaction/member_reaction_sender.rs
|
||||
src/session/view/content/room_history/message_toolbar/attachment_dialog.ui
|
||||
src/session/view/content/room_history/message_toolbar/mod.rs
|
||||
src/session/view/content/room_history/message_toolbar/mod.ui
|
||||
|
|
|
@ -214,7 +214,8 @@ impl MessageRow {
|
|||
)));
|
||||
self.update_content(&event);
|
||||
|
||||
imp.reactions.set_reaction_list(event.reactions());
|
||||
imp.reactions
|
||||
.set_reaction_list(&event.room().get_or_create_members(), event.reactions());
|
||||
imp.read_receipts
|
||||
.set_list(&event.room(), event.read_receipts());
|
||||
imp.event.replace(Some(event));
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
use adw::subclass::prelude::*;
|
||||
use gtk::{glib, prelude::*, CompositeTemplate};
|
||||
|
||||
use crate::{session::model::ReactionGroup, utils::EMOJI_REGEX};
|
||||
|
||||
mod imp {
|
||||
use glib::subclass::InitializingObject;
|
||||
use once_cell::{sync::Lazy, unsync::OnceCell};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, CompositeTemplate)]
|
||||
#[template(
|
||||
resource = "/org/gnome/Fractal/ui/session/view/content/room_history/message_row/reaction.ui"
|
||||
)]
|
||||
pub struct MessageReaction {
|
||||
/// The reaction group to display.
|
||||
pub group: OnceCell<ReactionGroup>,
|
||||
#[template_child]
|
||||
pub button: TemplateChild<gtk::ToggleButton>,
|
||||
#[template_child]
|
||||
pub reaction_key: TemplateChild<gtk::Label>,
|
||||
#[template_child]
|
||||
pub reaction_count: TemplateChild<gtk::Label>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for MessageReaction {
|
||||
const NAME: &'static str = "ContentMessageReaction";
|
||||
type Type = super::MessageReaction;
|
||||
type ParentType = gtk::FlowBoxChild;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Self::bind_template(klass);
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
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()]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
match pspec.name() {
|
||||
"group" => {
|
||||
self.obj().set_group(value.get().unwrap());
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"group" => self.obj().group().to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for MessageReaction {}
|
||||
|
||||
impl FlowBoxChildImpl for MessageReaction {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// A widget displaying the reactions of a message.
|
||||
pub struct MessageReaction(ObjectSubclass<imp::MessageReaction>)
|
||||
@extends gtk::Widget, gtk::FlowBoxChild, @implements gtk::Accessible;
|
||||
}
|
||||
|
||||
impl MessageReaction {
|
||||
pub fn new(reaction_group: ReactionGroup) -> Self {
|
||||
glib::Object::builder()
|
||||
.property("group", &reaction_group)
|
||||
.build()
|
||||
}
|
||||
|
||||
/// The reaction group to display.
|
||||
pub fn group(&self) -> Option<&ReactionGroup> {
|
||||
self.imp().group.get()
|
||||
}
|
||||
|
||||
/// 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();
|
||||
|
||||
imp.group.set(group).unwrap();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
use adw::subclass::prelude::*;
|
||||
use gettextrs::gettext;
|
||||
use gtk::{glib, prelude::*};
|
||||
|
||||
use crate::session::model::Member;
|
||||
|
||||
mod imp {
|
||||
use std::cell::Cell;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct MemberReactionSender {
|
||||
/// The room member of this reaction sender.
|
||||
pub member: glib::WeakRef<Member>,
|
||||
/// The timestamp of when the reaction was sent, in seconds since Unix
|
||||
/// Epoch, if any.
|
||||
///
|
||||
/// A value of 0 means no timestamp.
|
||||
pub timestamp: Cell<u64>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for MemberReactionSender {
|
||||
const NAME: &'static str = "ContentMemberReactionSender";
|
||||
type Type = super::MemberReactionSender;
|
||||
}
|
||||
|
||||
impl ObjectImpl for MemberReactionSender {
|
||||
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(),
|
||||
glib::ParamSpecString::builder("datetime")
|
||||
.read_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(),
|
||||
"datetime" => obj.datetime().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::wrapper! {
|
||||
/// A reaction sender's room member.
|
||||
pub struct MemberReactionSender(ObjectSubclass<imp::MemberReactionSender>);
|
||||
}
|
||||
|
||||
impl MemberReactionSender {
|
||||
/// Constructs a new `MemberReactionSender` with the given member.
|
||||
pub fn new(member: &Member, timestamp: u64) -> Self {
|
||||
glib::Object::builder()
|
||||
.property("member", member)
|
||||
.property("timestamp", timestamp)
|
||||
.build()
|
||||
}
|
||||
|
||||
/// The room member of this reaction sender.
|
||||
pub fn member(&self) -> Option<Member> {
|
||||
self.imp().member.upgrade()
|
||||
}
|
||||
|
||||
/// Set the room member of this reaction sender.
|
||||
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 when the reaction was sent, 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 when the reaction was sent.
|
||||
pub fn set_timestamp(&self, ts: u64) {
|
||||
if self.timestamp() == ts {
|
||||
return;
|
||||
}
|
||||
|
||||
self.imp().timestamp.set(ts);
|
||||
self.notify("timestamp");
|
||||
}
|
||||
|
||||
/// The formatted date and time of when the reaction was sent.
|
||||
pub fn datetime(&self) -> String {
|
||||
let timestamp = self.timestamp();
|
||||
|
||||
if timestamp == 0 {
|
||||
// No timestamp.
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let datetime = glib::DateTime::from_unix_utc(timestamp as i64)
|
||||
.and_then(|t| t.to_local())
|
||||
.unwrap();
|
||||
|
||||
// FIXME: Use system setting.
|
||||
let local_time = datetime.format("%X").unwrap().as_str().to_ascii_lowercase();
|
||||
let is_12h_format = local_time.ends_with("am") || local_time.ends_with("pm");
|
||||
|
||||
let format = if is_12h_format {
|
||||
// Translators: this is a date and a time in 12h format.
|
||||
// For example, "May 5 at 1:20 PM".
|
||||
// Do not change the time format as it will follow the system settings.
|
||||
// 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("%B %-e at %-l∶%M %p")
|
||||
} else {
|
||||
// Translators: this is a date and a time in 24h format.
|
||||
// For example, "May 5 at 13:20".
|
||||
// Do not change the time format as it will follow the system settings.
|
||||
// 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("%B %-e at %-k∶%M %p")
|
||||
};
|
||||
datetime.format(&format).unwrap().to_string()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,238 @@
|
|||
use adw::subclass::prelude::*;
|
||||
use gtk::{gio, glib, glib::clone, prelude::*, CompositeTemplate};
|
||||
use matrix_sdk_ui::timeline::ReactionSenderData as SdkReactionSenderData;
|
||||
|
||||
mod member_reaction_sender;
|
||||
mod reaction_popover;
|
||||
mod reaction_sender_row;
|
||||
|
||||
use self::{
|
||||
member_reaction_sender::MemberReactionSender, reaction_popover::ReactionPopover,
|
||||
reaction_sender_row::ReactionSenderRow,
|
||||
};
|
||||
use crate::{
|
||||
session::model::{MemberList, ReactionGroup},
|
||||
utils::{BoundObjectWeakRef, EMOJI_REGEX},
|
||||
};
|
||||
|
||||
mod imp {
|
||||
use std::cell::RefCell;
|
||||
|
||||
use glib::subclass::InitializingObject;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, CompositeTemplate)]
|
||||
#[template(
|
||||
resource = "/org/gnome/Fractal/ui/session/view/content/room_history/message_row/reaction/mod.ui"
|
||||
)]
|
||||
pub struct MessageReaction {
|
||||
/// The reaction senders (group) to display.
|
||||
pub group: BoundObjectWeakRef<ReactionGroup>,
|
||||
/// The list of reaction senders as room members.
|
||||
pub list: gio::ListStore,
|
||||
/// The member list of the room of the reaction.
|
||||
pub members: RefCell<Option<MemberList>>,
|
||||
#[template_child]
|
||||
pub button: TemplateChild<gtk::ToggleButton>,
|
||||
#[template_child]
|
||||
pub reaction_key: TemplateChild<gtk::Label>,
|
||||
#[template_child]
|
||||
pub reaction_count: TemplateChild<gtk::Label>,
|
||||
}
|
||||
|
||||
impl Default for MessageReaction {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
group: Default::default(),
|
||||
list: gio::ListStore::new::<MemberReactionSender>(),
|
||||
members: Default::default(),
|
||||
button: Default::default(),
|
||||
reaction_key: Default::default(),
|
||||
reaction_count: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for MessageReaction {
|
||||
const NAME: &'static str = "ContentMessageReaction";
|
||||
type Type = super::MessageReaction;
|
||||
type ParentType = gtk::FlowBoxChild;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Self::bind_template(klass);
|
||||
Self::Type::bind_template_callbacks(klass);
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
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!(),
|
||||
}
|
||||
}
|
||||
|
||||
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!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn dispose(&self) {
|
||||
self.group.disconnect_signals();
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for MessageReaction {}
|
||||
|
||||
impl FlowBoxChildImpl for MessageReaction {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// A widget displaying the reactions of a message.
|
||||
pub struct MessageReaction(ObjectSubclass<imp::MessageReaction>)
|
||||
@extends gtk::Widget, gtk::FlowBoxChild, @implements gtk::Accessible;
|
||||
}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl MessageReaction {
|
||||
pub fn new(members: MemberList, reaction_group: ReactionGroup) -> Self {
|
||||
glib::Object::builder()
|
||||
.property("group", reaction_group)
|
||||
.property("members", members)
|
||||
.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;
|
||||
};
|
||||
|
||||
let mut new_senders = Vec::with_capacity(added as usize);
|
||||
for i in pos..pos + added {
|
||||
let Some(boxed) = group.item(i).and_downcast::<glib::BoxedAnyObject>() else {
|
||||
break;
|
||||
};
|
||||
|
||||
let sender_data = boxed.borrow::<SdkReactionSenderData>();
|
||||
let member = members.get_or_create(sender_data.sender_id.clone());
|
||||
let timestamp = sender_data.timestamp.as_secs().into();
|
||||
let sender = MemberReactionSender::new(&member, timestamp);
|
||||
|
||||
new_senders.push(sender);
|
||||
}
|
||||
|
||||
self.list().splice(pos, removed, &new_senders);
|
||||
}
|
||||
|
||||
/// Handle a right click/long press on the reaction button.
|
||||
///
|
||||
/// 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 {
|
||||
// No popover.
|
||||
return;
|
||||
};
|
||||
|
||||
let button = &*self.imp().button;
|
||||
let popover = ReactionPopover::new(self.list());
|
||||
popover.set_parent(button);
|
||||
popover.connect_closed(clone!(@weak button => move |popover| {
|
||||
popover.unparent();
|
||||
}));
|
||||
popover.popup();
|
||||
}
|
||||
}
|
|
@ -28,7 +28,22 @@
|
|||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkGestureClick" id="click_gesture">
|
||||
<property name="button">3</property>
|
||||
<property name="exclusive">True</property>
|
||||
<signal name="released" handler="show_popover" swapped="true"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkGestureLongPress" id="long_press_gesture">
|
||||
<property name="touch_only">True</property>
|
||||
<property name="exclusive">True</property>
|
||||
<signal name="pressed" handler="show_popover" swapped="true"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
use adw::subclass::prelude::*;
|
||||
use gtk::{gio, glib, prelude::*, CompositeTemplate};
|
||||
|
||||
use super::ReactionSenderRow;
|
||||
|
||||
mod imp {
|
||||
use glib::subclass::InitializingObject;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, CompositeTemplate)]
|
||||
#[template(
|
||||
resource = "/org/gnome/Fractal/ui/session/view/content/room_history/message_row/reaction/reaction_popover.ui"
|
||||
)]
|
||||
pub struct ReactionPopover {
|
||||
#[template_child]
|
||||
pub list: TemplateChild<gtk::ListView>,
|
||||
/// The reaction senders to display.
|
||||
pub senders: glib::WeakRef<gio::ListStore>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for ReactionPopover {
|
||||
const NAME: &'static str = "ContentMessageReactionPopover";
|
||||
type Type = super::ReactionPopover;
|
||||
type ParentType = gtk::Popover;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
ReactionSenderRow::static_type();
|
||||
Self::bind_template(klass);
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for ReactionPopover {}
|
||||
|
||||
impl PopoverImpl for ReactionPopover {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// A popover to display the senders of a reaction.
|
||||
pub struct ReactionPopover(ObjectSubclass<imp::ReactionPopover>)
|
||||
@extends gtk::Widget, gtk::Popover;
|
||||
}
|
||||
|
||||
impl ReactionPopover {
|
||||
/// Constructs a new `ReactionPopover` with the given reaction senders.
|
||||
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");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="ContentMessageReactionPopover" parent="GtkPopover">
|
||||
<style>
|
||||
<class name="list-popover"/>
|
||||
</style>
|
||||
<property name="autohide">true</property>
|
||||
<property name="width-request">260</property>
|
||||
<property name="child">
|
||||
<object class="GtkScrolledWindow" id="scrolled_window">
|
||||
<property name="propagate-natural-height">true</property>
|
||||
<property name="hscrollbar-policy">never</property>
|
||||
<property name="max-content-height">280</property>
|
||||
<property name="child">
|
||||
<object class="GtkListView" id="list">
|
||||
<property name="factory">
|
||||
<object class="GtkBuilderListItemFactory">
|
||||
<property name="bytes"><![CDATA[
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="GtkListItem">
|
||||
<property name="activatable">False</property>
|
||||
<property name="selectable">False</property>
|
||||
<property name="child">
|
||||
<object class="ContentMessageReactionSenderRow">
|
||||
<binding name="sender">
|
||||
<lookup name="item">GtkListItem</lookup>
|
||||
</binding>
|
||||
</object>
|
||||
</property>
|
||||
</template>
|
||||
</interface>
|
||||
]]></property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</template>
|
||||
</interface>
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gtk::{glib, CompositeTemplate};
|
||||
|
||||
use super::member_reaction_sender::MemberReactionSender;
|
||||
|
||||
mod imp {
|
||||
use glib::subclass::InitializingObject;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, CompositeTemplate)]
|
||||
#[template(
|
||||
resource = "/org/gnome/Fractal/ui/session/view/content/room_history/message_row/reaction/reaction_sender_row.ui"
|
||||
)]
|
||||
pub struct ReactionSenderRow {
|
||||
/// The sender presented by this row.
|
||||
pub sender: glib::WeakRef<MemberReactionSender>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for ReactionSenderRow {
|
||||
const NAME: &'static str = "ContentMessageReactionSenderRow";
|
||||
type Type = super::ReactionSenderRow;
|
||||
type ParentType = adw::Bin;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Self::bind_template(klass);
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for ReactionSenderRow {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
glib::ParamSpecObject::builder::<MemberReactionSender>("sender")
|
||||
.explicit_notify()
|
||||
.build(),
|
||||
]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
match pspec.name() {
|
||||
"sender" => self.obj().set_sender(
|
||||
value
|
||||
.get::<Option<MemberReactionSender>>()
|
||||
.unwrap()
|
||||
.as_ref(),
|
||||
),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"sender" => self.obj().sender().to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for ReactionSenderRow {}
|
||||
|
||||
impl BinImpl for ReactionSenderRow {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// A row displaying a reaction sender.
|
||||
pub struct ReactionSenderRow(ObjectSubclass<imp::ReactionSenderRow>)
|
||||
@extends gtk::Widget, adw::Bin;
|
||||
}
|
||||
|
||||
impl ReactionSenderRow {
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new()
|
||||
}
|
||||
|
||||
/// The reaction sender presented by this row.
|
||||
pub fn sender(&self) -> Option<MemberReactionSender> {
|
||||
self.imp().sender.upgrade()
|
||||
}
|
||||
|
||||
/// Set the reaction sender presented by this row.
|
||||
pub fn set_sender(&self, sender: Option<&MemberReactionSender>) {
|
||||
if self.sender().as_ref() == sender {
|
||||
return;
|
||||
}
|
||||
|
||||
self.imp().sender.set(sender);
|
||||
self.notify("sender");
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ReactionSenderRow {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="ContentMessageReactionSenderRow" parent="AdwBin">
|
||||
<style>
|
||||
<class name="list-popover-row"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="spacing">10</property>
|
||||
<child>
|
||||
<object class="ComponentsAvatar">
|
||||
<property name="size">36</property>
|
||||
<binding name="data">
|
||||
<lookup name="avatar-data">
|
||||
<lookup name="member">
|
||||
<lookup name="sender">ContentMessageReactionSenderRow</lookup>
|
||||
</lookup>
|
||||
</lookup>
|
||||
</binding>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="spacing">3</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="xalign">0.0</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<binding name="label">
|
||||
<lookup name="display-name">
|
||||
<lookup name="member">
|
||||
<lookup name="sender">ContentMessageReactionSenderRow</lookup>
|
||||
</lookup>
|
||||
</lookup>
|
||||
</binding>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="xalign">1.0</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<binding name="label">
|
||||
<lookup name="datetime">
|
||||
<lookup name="sender">ContentMessageReactionSenderRow</lookup>
|
||||
</lookup>
|
||||
</binding>
|
||||
<style>
|
||||
<class name="dim-label"/>
|
||||
<class name="caption"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
use adw::subclass::prelude::*;
|
||||
use gtk::{glib, prelude::*, CompositeTemplate};
|
||||
use gtk::{glib, glib::clone, prelude::*, CompositeTemplate};
|
||||
|
||||
use super::reaction::MessageReaction;
|
||||
use crate::session::model::ReactionList;
|
||||
use crate::session::model::{MemberList, ReactionList};
|
||||
|
||||
mod imp {
|
||||
use glib::subclass::InitializingObject;
|
||||
|
@ -52,9 +52,15 @@ impl MessageReactionList {
|
|||
glib::Object::new()
|
||||
}
|
||||
|
||||
pub fn set_reaction_list(&self, reaction_list: &ReactionList) {
|
||||
self.imp().flow_box.bind_model(Some(reaction_list), |obj| {
|
||||
MessageReaction::new(obj.clone().downcast().unwrap()).upcast()
|
||||
});
|
||||
pub fn set_reaction_list(&self, members: &MemberList, reaction_list: &ReactionList) {
|
||||
self.imp().flow_box.bind_model(
|
||||
Some(reaction_list),
|
||||
clone!(
|
||||
@weak members => @default-return { gtk::FlowBoxChild::new().upcast() },
|
||||
move |obj| {
|
||||
MessageReaction::new(members, obj.clone().downcast().unwrap()).upcast()
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,7 +67,9 @@
|
|||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_row/location.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_row/media.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_row/mod.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_row/reaction.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_row/reaction/mod.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_row/reaction/reaction_popover.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_row/reaction/reaction_sender_row.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_row/reaction_list.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_row/reply.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_history/message_toolbar/attachment_dialog.ui</file>
|
||||
|
@ -100,3 +102,4 @@
|
|||
<file compressed="true" preprocess="xml-stripblanks">window.ui</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
|
||||
|
|
Loading…
Reference in a new issue