Browse Source

components: Rename ReactionChooser to QuickReactionChooser and refactor

pipelines/767384
Kévin Commaille 1 year ago
parent
commit
fcbdb5da1c
No known key found for this signature in database
GPG Key ID: C971D9DBC9D678D
  1. 2
      po/POTFILES.in
  2. 4
      src/components/mod.rs
  3. 251
      src/components/quick_reaction_chooser.rs
  4. 4
      src/components/quick_reaction_chooser.ui
  5. 207
      src/components/reaction_chooser.rs
  6. 2
      src/session/view/content/room_history/event_actions.ui
  7. 65
      src/session/view/content/room_history/item_row.rs
  8. 8
      src/session/view/content/room_history/mod.rs
  9. 2
      src/ui-resources.gresource.xml

2
po/POTFILES.in

@ -25,7 +25,7 @@ src/components/dialogs/user_profile.ui
src/components/offline_banner.rs
src/components/media/content_viewer.rs
src/components/media/location_viewer.rs
src/components/reaction_chooser.ui
src/components/quick_reaction_chooser.ui
src/components/pill/at_room.rs
src/components/power_level_selection/popover.ui
src/components/rows/loading_row.ui

4
src/components/mod.rs

@ -11,7 +11,7 @@ mod media;
mod offline_banner;
mod pill;
mod power_level_selection;
mod reaction_chooser;
mod quick_reaction_chooser;
mod role_badge;
mod rows;
mod scale_revealer;
@ -30,7 +30,7 @@ pub use self::{
offline_banner::OfflineBanner,
pill::*,
power_level_selection::*,
reaction_chooser::ReactionChooser,
quick_reaction_chooser::QuickReactionChooser,
role_badge::RoleBadge,
rows::*,
scale_revealer::ScaleRevealer,

251
src/components/quick_reaction_chooser.rs

@ -0,0 +1,251 @@
use adw::subclass::prelude::*;
use gtk::{
glib,
glib::{clone, closure_local},
prelude::*,
CompositeTemplate,
};
use crate::{session::model::ReactionList, utils::BoundObject};
/// A quick reaction.
#[derive(Debug, Clone, Copy)]
struct QuickReaction {
/// The emoji that is presented.
key: &'static str,
/// The number of the column where this reaction is presented.
///
/// There are 4 columns in total.
column: i32,
/// The number of the row where this reaction is presented.
///
/// There are 2 rows in total.
row: i32,
}
/// The quick reactions to present.
static QUICK_REACTIONS: &[QuickReaction] = &[
QuickReaction {
key: "👍",
column: 0,
row: 0,
},
QuickReaction {
key: "👎",
column: 1,
row: 0,
},
QuickReaction {
key: "😄",
column: 2,
row: 0,
},
QuickReaction {
key: "🎉",
column: 3,
row: 0,
},
QuickReaction {
key: "😕",
column: 0,
row: 1,
},
QuickReaction {
key: "❤",
column: 1,
row: 1,
},
QuickReaction {
key: "🚀",
column: 2,
row: 1,
},
];
mod imp {
use std::{cell::RefCell, collections::HashMap, sync::LazyLock};
use glib::subclass::{InitializingObject, Signal};
use super::*;
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(resource = "/org/gnome/Fractal/ui/components/quick_reaction_chooser.ui")]
#[properties(wrapper_type = super::QuickReactionChooser)]
pub struct QuickReactionChooser {
/// The list of reactions of the event for which this chooser is
/// presented.
#[property(get, set = Self::set_reactions, explicit_notify, nullable)]
reactions: BoundObject<ReactionList>,
reaction_bindings: RefCell<HashMap<String, glib::Binding>>,
#[template_child]
reaction_grid: TemplateChild<gtk::Grid>,
}
#[glib::object_subclass]
impl ObjectSubclass for QuickReactionChooser {
const NAME: &'static str = "QuickReactionChooser";
type Type = super::QuickReactionChooser;
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::bind_template_callbacks(klass);
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for QuickReactionChooser {
fn signals() -> &'static [Signal] {
static SIGNALS: LazyLock<Vec<Signal>> =
LazyLock::new(|| vec![Signal::builder("more-reactions-activated").build()]);
SIGNALS.as_ref()
}
fn constructed(&self) {
self.parent_constructed();
// Construct the quick reactions.
let grid = &self.reaction_grid;
for reaction in QUICK_REACTIONS {
let button = gtk::ToggleButton::builder()
.label(reaction.key)
.action_name("event.toggle-reaction")
.action_target(&reaction.key.to_variant())
.css_classes(["flat", "circular"])
.build();
button.connect_clicked(|button| {
button.activate_action("context-menu.close", None).unwrap();
});
grid.attach(&button, reaction.column, reaction.row, 1, 1);
}
}
}
impl WidgetImpl for QuickReactionChooser {}
impl BinImpl for QuickReactionChooser {}
#[gtk::template_callbacks]
impl QuickReactionChooser {
/// Set the list of reactions of the event for which this chooser is
/// presented.
fn set_reactions(&self, reactions: Option<ReactionList>) {
let prev_reactions = self.reactions.obj();
if prev_reactions == reactions {
return;
}
self.reactions.disconnect_signals();
for (_, binding) in self.reaction_bindings.borrow_mut().drain() {
binding.unbind();
}
// Reset the state of the buttons.
for row in 0..=1 {
for column in 0..=3 {
if let Some(button) = self
.reaction_grid
.child_at(column, row)
.and_downcast::<gtk::ToggleButton>()
{
button.set_active(false);
}
}
}
if let Some(reactions) = reactions {
let signal_handler = reactions.connect_items_changed(clone!(
#[weak(rename_to = imp)]
self,
move |_, _, _, _| {
imp.update_reactions();
}
));
self.reactions.set(reactions, vec![signal_handler]);
}
self.update_reactions();
}
/// Update the state of the quick reactions.
fn update_reactions(&self) {
let mut reaction_bindings = self.reaction_bindings.borrow_mut();
let reactions = self.reactions.obj();
for reaction_item in QUICK_REACTIONS {
if let Some(reaction) = reactions
.as_ref()
.and_then(|reactions| reactions.reaction_group_by_key(reaction_item.key))
{
if reaction_bindings.get(reaction_item.key).is_none() {
let button = self
.reaction_grid
.child_at(reaction_item.column, reaction_item.row)
.unwrap();
let binding = reaction
.bind_property("has-user", &button, "active")
.sync_create()
.build();
reaction_bindings.insert(reaction_item.key.to_string(), binding);
}
} else if let Some(binding) = reaction_bindings.remove(reaction_item.key) {
if let Some(button) = self
.reaction_grid
.child_at(reaction_item.column, reaction_item.row)
.and_downcast::<gtk::ToggleButton>()
{
button.set_active(false);
}
binding.unbind();
}
}
}
/// Handle when the "More reactions" button is activated.
#[template_callback]
fn more_reactions_activated(&self) {
self.obj()
.emit_by_name::<()>("more-reactions-activated", &[]);
}
}
}
glib::wrapper! {
/// A widget displaying quick reactions and taking its state from a [`ReactionList`].
pub struct QuickReactionChooser(ObjectSubclass<imp::QuickReactionChooser>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
impl QuickReactionChooser {
pub fn new() -> Self {
glib::Object::new()
}
/// Connect to the signal emitted when the "More reactions" button is
/// activated.
pub(crate) fn connect_more_reactions_activated<F: Fn(&Self) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.connect_closure(
"more-reactions-activated",
true,
closure_local!(move |obj: Self| {
f(&obj);
}),
)
}
}
impl Default for QuickReactionChooser {
fn default() -> Self {
Self::new()
}
}

4
src/components/reaction_chooser.ui → src/components/quick_reaction_chooser.ui

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="ReactionChooser" parent="AdwBin">
<template class="QuickReactionChooser" parent="AdwBin">
<property name="child">
<object class="GtkGrid" id="reaction_grid">
<accessibility>
@ -18,8 +18,8 @@
<style>
<class name="circular"/>
</style>
<property name="action_name">event.more-reactions</property>
<property name="icon_name">view-more-horizontal-symbolic</property>
<signal name="clicked" handler="more_reactions_activated" swapped="yes" />
<layout>
<property name="column">3</property>
<property name="row">1</property>

207
src/components/reaction_chooser.rs

@ -1,207 +0,0 @@
use adw::subclass::prelude::*;
use gtk::{glib, glib::clone, prelude::*, CompositeTemplate};
use crate::session::model::ReactionList;
struct ReactionGridItem<'a> {
key: &'a str,
column: i32,
row: i32,
}
static QUICK_REACTIONS: &[ReactionGridItem] = &[
ReactionGridItem {
key: "👍",
column: 0,
row: 0,
},
ReactionGridItem {
key: "👎",
column: 1,
row: 0,
},
ReactionGridItem {
key: "😄",
column: 2,
row: 0,
},
ReactionGridItem {
key: "🎉",
column: 3,
row: 0,
},
ReactionGridItem {
key: "😕",
column: 0,
row: 1,
},
ReactionGridItem {
key: "❤",
column: 1,
row: 1,
},
ReactionGridItem {
key: "🚀",
column: 2,
row: 1,
},
];
mod imp {
use std::{cell::RefCell, collections::HashMap};
use glib::subclass::InitializingObject;
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/Fractal/ui/components/reaction_chooser.ui")]
pub struct ReactionChooser {
/// The `ReactionList` associated to this chooser
pub reactions: RefCell<Option<ReactionList>>,
pub reactions_handler: RefCell<Option<glib::SignalHandlerId>>,
pub reaction_bindings: RefCell<HashMap<String, glib::Binding>>,
#[template_child]
pub reaction_grid: TemplateChild<gtk::Grid>,
}
#[glib::object_subclass]
impl ObjectSubclass for ReactionChooser {
const NAME: &'static str = "ReactionChooser";
type Type = super::ReactionChooser;
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 ReactionChooser {
fn constructed(&self) {
self.parent_constructed();
let grid = &self.reaction_grid;
for reaction_item in QUICK_REACTIONS {
let button = gtk::ToggleButton::builder()
.label(reaction_item.key)
.action_name("event.toggle-reaction")
.action_target(&reaction_item.key.to_variant())
.css_classes(["flat", "circular"])
.build();
button.connect_clicked(|button| {
button.activate_action("context-menu.close", None).unwrap();
});
grid.attach(&button, reaction_item.column, reaction_item.row, 1, 1);
}
}
}
impl WidgetImpl for ReactionChooser {}
impl BinImpl for ReactionChooser {}
}
glib::wrapper! {
/// A widget displaying a `ReactionChooser` for a `ReactionList`.
pub struct ReactionChooser(ObjectSubclass<imp::ReactionChooser>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
impl ReactionChooser {
pub fn new() -> Self {
glib::Object::new()
}
pub fn reactions(&self) -> Option<ReactionList> {
self.imp().reactions.borrow().clone()
}
pub fn set_reactions(&self, reactions: Option<ReactionList>) {
let imp = self.imp();
let prev_reactions = self.reactions();
if prev_reactions == reactions {
return;
}
if let Some(reactions) = prev_reactions.as_ref() {
if let Some(signal_handler) = imp.reactions_handler.take() {
reactions.disconnect(signal_handler);
}
let mut reaction_bindings = imp.reaction_bindings.borrow_mut();
for reaction_item in QUICK_REACTIONS {
if let Some(binding) = reaction_bindings.remove(reaction_item.key) {
if let Some(button) = imp
.reaction_grid
.child_at(reaction_item.column, reaction_item.row)
.and_downcast::<gtk::ToggleButton>()
{
button.set_active(false);
}
binding.unbind();
}
}
}
if let Some(reactions) = reactions.as_ref() {
let signal_handler = reactions.connect_items_changed(clone!(
#[weak(rename_to = obj)]
self,
move |_, _, _, _| {
obj.update_reactions();
}
));
imp.reactions_handler.replace(Some(signal_handler));
}
imp.reactions.replace(reactions);
self.update_reactions();
}
fn update_reactions(&self) {
let imp = self.imp();
let mut reaction_bindings = imp.reaction_bindings.borrow_mut();
let reactions = self.reactions();
for reaction_item in QUICK_REACTIONS {
if let Some(reaction) = reactions
.as_ref()
.and_then(|reactions| reactions.reaction_group_by_key(reaction_item.key))
{
if reaction_bindings.get(reaction_item.key).is_none() {
let button = imp
.reaction_grid
.child_at(reaction_item.column, reaction_item.row)
.unwrap();
let binding = reaction
.bind_property("has-user", &button, "active")
.sync_create()
.build();
reaction_bindings.insert(reaction_item.key.to_string(), binding);
}
} else if let Some(binding) = reaction_bindings.remove(reaction_item.key) {
if let Some(button) = imp
.reaction_grid
.child_at(reaction_item.column, reaction_item.row)
.and_downcast::<gtk::ToggleButton>()
{
button.set_active(false);
}
binding.unbind();
}
}
}
}
impl Default for ReactionChooser {
fn default() -> Self {
Self::new()
}
}

2
src/session/view/content/room_history/event_actions.ui

@ -3,7 +3,7 @@
<menu id="message_menu_model_with_reactions">
<section>
<item>
<attribute name="custom">reaction-chooser</attribute>
<attribute name="custom">quick-reaction-chooser</attribute>
</item>
</section>
<section>

65
src/session/view/content/room_history/item_row.rs

@ -9,7 +9,7 @@ use tracing::error;
use super::{DividerRow, MessageRow, RoomHistory, StateRow, TypingRow};
use crate::{
components::{ContextMenuBin, ContextMenuBinExt, ContextMenuBinImpl, ReactionChooser},
components::{ContextMenuBin, ContextMenuBinExt, ContextMenuBinImpl},
prelude::*,
session::{
model::{Event, EventKey, MessageState, Room, TimelineItem, VirtualItem, VirtualItemKind},
@ -40,7 +40,6 @@ mod imp {
pub event_handlers: RefCell<Vec<glib::SignalHandlerId>>,
pub permissions_handler: RefCell<Option<glib::SignalHandlerId>>,
pub binding: RefCell<Option<glib::Binding>>,
pub reaction_chooser: RefCell<Option<ReactionChooser>>,
pub emoji_chooser: RefCell<Option<gtk::EmojiChooser>>,
}
@ -94,45 +93,56 @@ mod imp {
impl ContextMenuBinImpl for ItemRow {
fn menu_opened(&self) {
let obj = self.obj();
let Some(room_history) = self.room_history.upgrade() else {
return;
};
let obj = self.obj();
let Some(event) = self.item.borrow().clone().and_downcast::<Event>() else {
obj.set_popover(None);
return;
};
let Some(action_group) = self.action_group.borrow().clone() else {
if self.action_group.borrow().is_none() {
// There are no possible actions.
obj.set_popover(None);
return;
};
let Some(room_history) = obj.room_history() else {
return;
};
let popover = room_history.item_context_menu().to_owned();
room_history.enable_sticky_mode(false);
obj.add_css_class("has-open-popup");
let cell: Rc<RefCell<Option<glib::signal::SignalHandlerId>>> =
Rc::new(RefCell::new(None));
let signal_id = popover.connect_closed(clone!(
let closed_handler_cell: Rc<RefCell<Option<glib::signal::SignalHandlerId>>> =
Rc::default();
let quick_reaction_chooser_handler_cell: Rc<
RefCell<Option<glib::signal::SignalHandlerId>>,
> = Rc::default();
let closed_handler = popover.connect_closed(clone!(
#[weak]
obj,
#[strong]
cell,
#[weak]
room_history,
#[strong]
closed_handler_cell,
#[strong]
quick_reaction_chooser_handler_cell,
move |popover| {
room_history.enable_sticky_mode(true);
obj.remove_css_class("has-open-popup");
if let Some(signal_id) = cell.take() {
popover.disconnect(signal_id);
if let Some(handler) = closed_handler_cell.take() {
popover.disconnect(handler);
}
if let Some(handler) = quick_reaction_chooser_handler_cell.take() {
room_history
.item_quick_reaction_chooser()
.disconnect(handler);
}
}
));
cell.replace(Some(signal_id));
closed_handler_cell.replace(Some(closed_handler));
if let Some(event) = event
.downcast_ref::<Event>()
@ -151,22 +161,23 @@ mod imp {
}
if can_send_reaction {
let reaction_chooser = room_history.item_reaction_chooser();
reaction_chooser.set_reactions(Some(event.reactions()));
popover.add_child(reaction_chooser, "reaction-chooser");
let quick_reaction_chooser = room_history.item_quick_reaction_chooser();
quick_reaction_chooser.set_reactions(Some(event.reactions()));
popover.add_child(quick_reaction_chooser, "quick-reaction-chooser");
// Open emoji chooser
action_group.add_action_entries([gio::ActionEntry::builder("more-reactions")
.activate(clone!(
let quick_reaction_chooser_handler = quick_reaction_chooser
.connect_more_reactions_activated(clone!(
#[weak]
obj,
#[weak]
popover,
move |_, _, _| {
obj.show_emoji_chooser(&popover);
move |_| {
obj.show_reactions_chooser(&popover);
}
))
.build()]);
));
quick_reaction_chooser_handler_cell
.replace(Some(quick_reaction_chooser_handler));
}
} else {
let menu_model = event_state_menu_model();
@ -447,7 +458,8 @@ impl ItemRow {
self.remove_css_class("highlight");
}
fn show_emoji_chooser(&self, popover: &gtk::PopoverMenu) {
/// Replace the given popover with an emoji chooser for reactions.
fn show_reactions_chooser(&self, popover: &gtk::PopoverMenu) {
let (_, rectangle) = popover.pointing_to();
let emoji_chooser = gtk::EmojiChooser::builder()
@ -459,8 +471,7 @@ impl ItemRow {
#[weak(rename_to = obj)]
self,
move |_, emoji| {
obj.activate_action("event.toggle-reaction", Some(&emoji.to_variant()))
.unwrap();
let _ = obj.activate_action("event.toggle-reaction", Some(&emoji.to_variant()));
}
));
emoji_chooser.connect_closed(|emoji_chooser| {

8
src/session/view/content/room_history/mod.rs

@ -27,7 +27,7 @@ use self::{
};
use super::{room_details, RoomDetails};
use crate::{
components::{confirm_leave_room_dialog, DragOverlay, ReactionChooser},
components::{confirm_leave_room_dialog, DragOverlay, QuickReactionChooser},
prelude::*,
session::model::{
Event, EventKey, MemberList, Membership, ReceiptPosition, Room, RoomCategory, Timeline,
@ -87,7 +87,7 @@ mod imp {
#[template_child]
drag_overlay: TemplateChild<DragOverlay>,
pub(super) item_context_menu: OnceCell<gtk::PopoverMenu>,
pub(super) item_reaction_chooser: ReactionChooser,
pub(super) item_quick_reaction_chooser: QuickReactionChooser,
pub(super) sender_context_menu: OnceCell<gtk::PopoverMenu>,
/// The room currently displayed.
#[property(get, set = Self::set_room, explicit_notify, nullable)]
@ -1012,8 +1012,8 @@ impl RoomHistory {
}
/// The reaction chooser for the item rows.
pub fn item_reaction_chooser(&self) -> &ReactionChooser {
&self.imp().item_reaction_chooser
pub fn item_quick_reaction_chooser(&self) -> &QuickReactionChooser {
&self.imp().item_quick_reaction_chooser
}
/// The context menu for the sender avatars.

2
src/ui-resources.gresource.xml

@ -28,7 +28,7 @@
<file compressed="true" preprocess="xml-stripblanks">components/power_level_selection/combo_box.ui</file>
<file compressed="true" preprocess="xml-stripblanks">components/power_level_selection/popover.ui</file>
<file compressed="true" preprocess="xml-stripblanks">components/power_level_selection/row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">components/reaction_chooser.ui</file>
<file compressed="true" preprocess="xml-stripblanks">components/quick_reaction_chooser.ui</file>
<file compressed="true" preprocess="xml-stripblanks">components/rows/button_count_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">components/rows/check_loading_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">components/rows/combo_loading_row.ui</file>

Loading…
Cancel
Save