[fractal/fractal-next] content: Add reaction chooser to context menu



commit 0a47fb71a6239a37b3ec5ba5910a30f448f381af
Author: KΓ©vin Commaille <zecakeh tedomum fr>
Date:   Wed Jan 19 18:06:30 2022 +0100

    content: Add reaction chooser to context menu

 data/resources/resources.gresource.xml             |   1 +
 data/resources/style.css                           |  16 +-
 data/resources/ui/components-reaction-chooser.ui   |  27 +++
 data/resources/ui/event-menu.ui                    |  46 ++++++
 src/components/context_menu_bin.rs                 |  18 ++
 src/components/mod.rs                              |   2 +
 src/components/reaction_chooser.rs                 | 184 +++++++++++++++++++++
 src/session/content/room_history/item_row.rs       |  82 ++++++++-
 .../content/room_history/message_row/text.rs       |  11 +-
 src/session/room/event_actions.rs                  |  35 ++--
 src/session/room/reaction_list.rs                  |   5 +-
 11 files changed, 400 insertions(+), 27 deletions(-)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 6911d699..f700e4e1 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -62,6 +62,7 @@
     <file compressed="true" preprocess="xml-stripblanks" 
alias="identity-verification-widget.ui">ui/identity-verification-widget.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="qr-code-scanner.ui">ui/qr-code-scanner.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="components-video-player.ui">ui/components-video-player.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="components-reaction-chooser.ui">ui/components-reaction-chooser.ui</file>
     <file compressed="true">style.css</file>
     <file compressed="true">style-dark.css</file>
     <file preprocess="xml-stripblanks">icons/scalable/actions/send-symbolic.svg</file>
diff --git a/data/resources/style.css b/data/resources/style.css
index 18f62257..79dd4faa 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -310,8 +310,12 @@ message-reactions .toggle {
   border: 1px solid @light_4;
 }
 
-message-reactions .toggle:checked {
+message-reactions .toggle:checked,
+.reaction-chooser button:checked {
   background-color: alpha(@blue_1, 0.4);
+}
+
+message-reactions .toggle:checked {
   border-color: @blue_2;
 }
 
@@ -324,6 +328,16 @@ message-reactions .reaction-count {
   padding-left: 5px;
 }
 
+.reaction-chooser {
+  margin: 5px;
+}
+
+.reaction-chooser button {
+  font-size: 1.3em;
+  -gtk-icon-size: 1.3em;
+  padding: 2px;
+}
+
 .divider-row {
   font-size: 0.9em;
   font-weight: bold;
diff --git a/data/resources/ui/components-reaction-chooser.ui 
b/data/resources/ui/components-reaction-chooser.ui
new file mode 100644
index 00000000..5e1caa51
--- /dev/null
+++ b/data/resources/ui/components-reaction-chooser.ui
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="ComponentsReactionChooser" parent="AdwBin">
+    <property name="child">
+      <object class="GtkGrid" id="reaction_grid">
+        <property name="row-spacing">4</property>
+        <property name="column-spacing">4</property>
+        <style>
+          <class name="reaction-chooser"/>
+        </style>
+        <child>
+          <object class="GtkButton">
+            <style>
+              <class name="circular"/>
+            </style>
+            <property name="action_name">event.more-reactions</property>
+            <property name="icon_name">view-more-horizontal-symbolic</property>
+            <layout>
+              <property name="column">3</property>
+              <property name="row">1</property>
+            </layout>
+          </object>
+        </child>
+      </object>
+    </property>
+  </template>
+</interface>
\ No newline at end of file
diff --git a/data/resources/ui/event-menu.ui b/data/resources/ui/event-menu.ui
index 4970f0e6..48ebaa3d 100644
--- a/data/resources/ui/event-menu.ui
+++ b/data/resources/ui/event-menu.ui
@@ -1,6 +1,11 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
   <menu id="message_menu_model">
+    <section>
+      <item>
+        <attribute name="custom">reaction-chooser</attribute>
+      </item>
+    </section>
     <section>
       <item>
         <attribute name="label" translatable="yes">_Reply</attribute>
@@ -75,4 +80,45 @@
       </item>
     </section>
   </menu>
+  <menu id="state_menu_model">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Forward</attribute>
+        <attribute name="action">event.forward</attribute>
+        <attribute name="hidden-when">action-missing</attribute>
+      </item>
+    </section>
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Select</attribute>
+        <attribute name="action">event.select</attribute>
+        <attribute name="hidden-when">action-missing</attribute>
+      </item>
+    </section>
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Copy Text</attribute>
+        <attribute name="action">event.copy-text</attribute>
+        <attribute name="hidden-when">action-disabled</attribute>
+        <attribute name="hidden-when">action-missing</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Permalink</attribute>
+        <attribute name="action">event.permalink</attribute>
+        <attribute name="hidden-when">action-missing</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_View Source</attribute>
+        <attribute name="action">event.view-source</attribute>
+        <attribute name="hidden-when">action-missing</attribute>
+      </item>
+    </section>
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">Re_move</attribute>
+        <attribute name="action">event.remove</attribute>
+        <attribute name="hidden-when">action-missing</attribute>
+      </item>
+    </section>
+  </menu>
 </interface>
diff --git a/src/components/context_menu_bin.rs b/src/components/context_menu_bin.rs
index 0c44e3bb..3a47a6fb 100644
--- a/src/components/context_menu_bin.rs
+++ b/src/components/context_menu_bin.rs
@@ -57,6 +57,11 @@ mod imp {
                 "context-menu.activate",
                 None,
             );
+
+            klass.install_action("context-menu.close", None, move |widget, _, _| {
+                let priv_ = imp::ContextMenuBin::from_instance(widget);
+                priv_.popover.popdown();
+            });
         }
 
         fn instance_init(obj: &InitializingObject<Self>) {
@@ -171,11 +176,19 @@ pub trait ContextMenuBinExt: 'static {
 
     /// Get the `MenuModel` used in the context menu.
     fn context_menu(&self) -> Option<gio::MenuModel>;
+
+    /// Get the `PopoverMenu` used in the context menu.
+    fn popover(&self) -> &gtk::PopoverMenu;
 }
 
 impl<O: IsA<ContextMenuBin>> ContextMenuBinExt for O {
     fn set_context_menu(&self, menu: Option<&gio::MenuModel>) {
         let priv_ = imp::ContextMenuBin::from_instance(self.upcast_ref());
+
+        if self.context_menu().as_ref() == menu {
+            return;
+        }
+
         priv_.popover.set_menu_model(menu);
         self.notify("context-menu");
     }
@@ -184,6 +197,11 @@ impl<O: IsA<ContextMenuBin>> ContextMenuBinExt for O {
         let priv_ = imp::ContextMenuBin::from_instance(self.upcast_ref());
         priv_.popover.menu_model()
     }
+
+    fn popover(&self) -> &gtk::PopoverMenu {
+        let priv_ = imp::ContextMenuBin::from_instance(self.upcast_ref());
+        &priv_.popover
+    }
 }
 
 pub trait ContextMenuBinImpl: BinImpl {}
diff --git a/src/components/mod.rs b/src/components/mod.rs
index 5c005a97..81319af7 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -7,6 +7,7 @@ mod in_app_notification;
 mod label_with_widgets;
 mod loading_listbox_row;
 mod pill;
+mod reaction_chooser;
 mod room_title;
 mod spinner_button;
 mod video_player;
@@ -20,6 +21,7 @@ pub use self::in_app_notification::InAppNotification;
 pub use self::label_with_widgets::LabelWithWidgets;
 pub use self::loading_listbox_row::LoadingListBoxRow;
 pub use self::pill::Pill;
+pub use self::reaction_chooser::ReactionChooser;
 pub use self::room_title::RoomTitle;
 pub use self::spinner_button::SpinnerButton;
 pub use self::video_player::VideoPlayer;
diff --git a/src/components/reaction_chooser.rs b/src/components/reaction_chooser.rs
new file mode 100644
index 00000000..211f4a5c
--- /dev/null
+++ b/src/components/reaction_chooser.rs
@@ -0,0 +1,184 @@
+use adw::subclass::prelude::*;
+use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
+
+use crate::session::room::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 super::*;
+    use glib::subclass::InitializingObject;
+    use std::{cell::RefCell, collections::HashMap};
+
+    #[derive(Debug, Default, CompositeTemplate)]
+    #[template(resource = "/org/gnome/FractalNext/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 = "ComponentsReactionChooser";
+        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, obj: &Self::Type) {
+            self.parent_constructed(obj);
+
+            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(vec!["flat".to_string(), "circular".to_string()])
+                    .build();
+                button.connect_clicked(|button| {
+                    button.activate_action("context-menu.close", None);
+                });
+                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(&[]).expect("Failed to create ReactionChooser")
+    }
+
+    pub fn reactions(&self) -> Option<ReactionList> {
+        let priv_ = imp::ReactionChooser::from_instance(self);
+        priv_.reactions.borrow().clone()
+    }
+
+    pub fn set_reactions(&self, reactions: Option<ReactionList>) {
+        let priv_ = imp::ReactionChooser::from_instance(self);
+        let prev_reactions = self.reactions();
+
+        if prev_reactions == reactions {
+            return;
+        }
+
+        if let Some(reactions) = prev_reactions.as_ref() {
+            if let Some(signal_handler) = priv_.reactions_handler.take() {
+                reactions.disconnect(signal_handler);
+            }
+            for (_, binding) in priv_.reaction_bindings.borrow_mut().drain() {
+                binding.unbind();
+            }
+        }
+
+        if let Some(reactions) = reactions.as_ref() {
+            let signal_handler =
+                reactions.connect_items_changed(clone!(@weak self as obj => move |_, _, _, _| {
+                    obj.update_reactions();
+                }));
+            priv_.reactions_handler.replace(Some(signal_handler));
+        }
+        priv_.reactions.replace(reactions);
+        self.update_reactions();
+    }
+
+    fn update_reactions(&self) {
+        let priv_ = imp::ReactionChooser::from_instance(self);
+        let mut reaction_bindings = priv_.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 = priv_
+                        .reaction_grid
+                        .child_at(reaction_item.column, reaction_item.row)
+                        .unwrap();
+                    let binding = reaction
+                        .bind_property("has-user", &button, "active")
+                        .flags(glib::BindingFlags::SYNC_CREATE)
+                        .build()
+                        .unwrap();
+                    reaction_bindings.insert(reaction_item.key.to_string(), binding);
+                }
+            } else if let Some(binding) = reaction_bindings.remove(reaction_item.key) {
+                binding.unbind();
+            }
+        }
+    }
+}
+
+impl Default for ReactionChooser {
+    fn default() -> Self {
+        Self::new()
+    }
+}
diff --git a/src/session/content/room_history/item_row.rs b/src/session/content/room_history/item_row.rs
index 1b0c26d5..f32d806a 100644
--- a/src/session/content/room_history/item_row.rs
+++ b/src/session/content/room_history/item_row.rs
@@ -3,9 +3,9 @@ use gettextrs::gettext;
 use gtk::{gio, glib, glib::clone, subclass::prelude::*};
 use matrix_sdk::ruma::events::AnySyncRoomEvent;
 
-use crate::components::{ContextMenuBin, ContextMenuBinExt, ContextMenuBinImpl};
+use crate::components::{ContextMenuBin, ContextMenuBinExt, ContextMenuBinImpl, ReactionChooser};
 use crate::session::content::room_history::{message_row::MessageRow, DividerRow, StateRow};
-use crate::session::room::{Event, EventActions, Item, ItemType};
+use crate::session::room::{Event, EventActions, Item, ItemType, ReactionList};
 
 mod imp {
     use super::*;
@@ -17,6 +17,8 @@ mod imp {
         pub item: RefCell<Option<Item>>,
         pub menu_model: RefCell<Option<gio::MenuModel>>,
         pub event_notify_handler: RefCell<Option<SignalHandlerId>>,
+        pub reaction_chooser: RefCell<Option<ReactionChooser>>,
+        pub emoji_chooser: RefCell<Option<gtk::EmojiChooser>>,
     }
 
     #[glib::object_subclass]
@@ -65,7 +67,7 @@ mod imp {
             }
         }
 
-        fn dispose(&self, _obj: &Self::Type) {
+        fn dispose(&self, obj: &Self::Type) {
             if let Some(ItemType::Event(event)) =
                 self.item.borrow().as_ref().map(|item| item.type_())
             {
@@ -73,6 +75,8 @@ mod imp {
                     event.disconnect(handler);
                 }
             }
+
+            obj.remove_reaction_chooser();
         }
     }
 
@@ -116,10 +120,22 @@ impl ItemRow {
         if let Some(ref item) = item {
             match item.type_() {
                 ItemType::Event(event) => {
-                    if self.context_menu().is_none() {
+                    let action_group = self.set_event_actions(Some(event));
+
+                    if event.message_content().is_some() {
                         self.set_context_menu(Some(Self::event_message_menu_model()));
+                        self.set_reaction_chooser(event.reactions());
+
+                        // Open emoji chooser
+                        let more_reactions = gio::SimpleAction::new("more-reactions", None);
+                        more_reactions.connect_activate(clone!(@weak self as obj => move |_, _| {
+                            obj.show_emoji_chooser();
+                        }));
+                        action_group.unwrap().add_action(&more_reactions);
+                    } else {
+                        self.set_context_menu(Some(Self::event_state_menu_model()));
+                        self.remove_reaction_chooser();
                     }
-                    self.set_event_actions(Some(event));
 
                     let event_notify_handler = event.connect_notify_local(
                         Some("event"),
@@ -139,6 +155,7 @@ impl ItemRow {
                     if self.context_menu().is_some() {
                         self.set_context_menu(None);
                         self.set_event_actions(None);
+                        self.remove_reaction_chooser();
                     }
 
                     let fmt = if date.year() == glib::DateTime::new_now_local().unwrap().year() {
@@ -161,6 +178,7 @@ impl ItemRow {
                     if self.context_menu().is_some() {
                         self.set_context_menu(None);
                         self.set_event_actions(None);
+                        self.remove_reaction_chooser();
                     }
 
                     let label = gettext("New Messages");
@@ -216,6 +234,60 @@ impl ItemRow {
             }
         }
     }
+
+    /// Set the reaction chooser for the given `reactions`.
+    ///
+    /// If it doesn't exist, it is created
+    fn set_reaction_chooser(&self, reactions: &ReactionList) {
+        let priv_ = imp::ItemRow::from_instance(self);
+
+        if priv_.reaction_chooser.borrow().is_none() {
+            let reaction_chooser = ReactionChooser::new();
+            self.popover()
+                .add_child(&reaction_chooser, "reaction-chooser");
+            priv_.reaction_chooser.replace(Some(reaction_chooser));
+        }
+
+        priv_
+            .reaction_chooser
+            .borrow()
+            .as_ref()
+            .unwrap()
+            .set_reactions(Some(reactions.to_owned()));
+    }
+
+    /// Remove the reaction chooser and the emoji chooser, if they exist.
+    fn remove_reaction_chooser(&self) {
+        let priv_ = imp::ItemRow::from_instance(self);
+
+        if let Some(reaction_chooser) = priv_.reaction_chooser.take() {
+            reaction_chooser.unparent();
+        }
+
+        if let Some(emoji_chooser) = priv_.emoji_chooser.take() {
+            emoji_chooser.unparent();
+        }
+    }
+
+    fn show_emoji_chooser(&self) {
+        let priv_ = imp::ItemRow::from_instance(self);
+
+        if priv_.emoji_chooser.borrow().is_none() {
+            let emoji_chooser = gtk::EmojiChooser::builder().has_arrow(false).build();
+            emoji_chooser.connect_emoji_picked(|emoji_chooser, emoji| {
+                emoji_chooser.activate_action("event.toggle-reaction", Some(&emoji.to_variant()));
+            });
+            emoji_chooser.set_parent(self);
+            priv_.emoji_chooser.replace(Some(emoji_chooser));
+        }
+
+        let emoji_chooser = priv_.emoji_chooser.borrow().clone().unwrap();
+        if let Some(rectangle) = self.popover().pointing_to() {
+            emoji_chooser.set_pointing_to(&rectangle);
+        }
+        self.popover().popdown();
+        emoji_chooser.popup();
+    }
 }
 
 impl Default for ItemRow {
diff --git a/src/session/content/room_history/message_row/text.rs 
b/src/session/content/room_history/message_row/text.rs
index c23a5a22..19dbcd2d 100644
--- a/src/session/content/room_history/message_row/text.rs
+++ b/src/session/content/room_history/message_row/text.rs
@@ -10,11 +10,7 @@ use once_cell::sync::Lazy;
 use regex::Regex;
 use sourceview::prelude::*;
 
-use crate::session::{
-    content::room_history::ItemRow,
-    room::{EventActions, Member},
-    UserExt,
-};
+use crate::session::{room::Member, UserExt};
 
 static EMOJI_REGEX: Lazy<Regex> = Lazy::new(|| {
     Regex::new(
@@ -259,8 +255,9 @@ fn set_label_styles(w: &gtk::Label) {
     w.set_xalign(0.0);
     w.set_valign(gtk::Align::Start);
     w.set_halign(gtk::Align::Fill);
-    w.set_selectable(true);
-    w.set_extra_menu(Some(ItemRow::event_message_menu_model()));
+    // FIXME: We have to be able to allow text selection and override popover menu.
+    // See https://gitlab.gnome.org/GNOME/gtk/-/issues/4606
+    // w.set_selectable(true);
 }
 
 fn create_widget_for_html_block(block: &HtmlBlock) -> gtk::Widget {
diff --git a/src/session/room/event_actions.rs b/src/session/room/event_actions.rs
index 2fc9a625..e9421488 100644
--- a/src/session/room/event_actions.rs
+++ b/src/session/room/event_actions.rs
@@ -48,15 +48,27 @@ where
         &MODEL.0
     }
 
+    /// The default `MenuModel` for common state event actions.
+    fn event_state_menu_model() -> &'static gio::MenuModel {
+        static MODEL: Lazy<MenuModelSendSync> = Lazy::new(|| {
+            MenuModelSendSync(
+                gtk::Builder::from_resource("/org/gnome/FractalNext/event-menu.ui")
+                    .object::<gio::MenuModel>("state_menu_model")
+                    .unwrap(),
+            )
+        });
+        &MODEL.0
+    }
+
     /// Set the actions available on `self` for `event`.
     ///
     /// Unsets the actions if `event` is `None`.
     ///
-    /// Should be used with the compatible model from `event_menu_model`.
-    fn set_event_actions(&self, event: Option<&Event>) {
+    /// Should be paired with the `EventActions` menu models.
+    fn set_event_actions(&self, event: Option<&Event>) -> Option<gio::SimpleActionGroup> {
         if event.is_none() {
             self.insert_action_group("event", gio::NONE_ACTION_GROUP);
-            return;
+            return None;
         }
 
         let event = event.unwrap();
@@ -79,15 +91,15 @@ where
                 let key: String = variant.unwrap().get().unwrap();
                 let room = event.room();
 
-                    let reaction_group = event.reactions().reaction_group_by_key(&key);
+                let reaction_group = event.reactions().reaction_group_by_key(&key);
 
-                    if let Some(reaction) = reaction_group.and_then(|group| group.user_reaction()) {
-                        // The user already sent that reaction, redact it.
-                        room.redact(reaction.matrix_event_id(), None);
-                    } else {
-                        // The user didn't send that redaction, send it.
-                        room.send_reaction(key, event.matrix_event_id());
-                    }
+                if let Some(reaction) = reaction_group.and_then(|group| group.user_reaction()) {
+                    // The user already sent that reaction, redact it.
+                    room.redact(reaction.matrix_event_id(), None);
+                } else {
+                    // The user didn't send that redaction, send it.
+                    room.send_reaction(key, event.matrix_event_id());
+                }
             }));
             action_group.add_action(&toggle_reaction);
 
@@ -113,6 +125,7 @@ where
         }
 
         self.insert_action_group("event", Some(&action_group));
+        Some(action_group)
     }
 
     /// Save the file in `event`.
diff --git a/src/session/room/reaction_list.rs b/src/session/room/reaction_list.rs
index cef6806b..bf7532d2 100644
--- a/src/session/room/reaction_list.rs
+++ b/src/session/room/reaction_list.rs
@@ -119,9 +119,8 @@ impl ReactionList {
     /// Remove a reaction group by its key.
     pub fn remove_reaction_group(&self, key: &str) {
         let priv_ = imp::ReactionList::from_instance(self);
-        if let Some((pos, _, _)) = priv_.reactions.borrow_mut().shift_remove_full(key) {
-            self.items_changed(pos as u32, 1, 0);
-        }
+        let (pos, ..) = priv_.reactions.borrow_mut().shift_remove_full(key).unwrap();
+        self.items_changed(pos as u32, 1, 0);
     }
 }
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]