Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,12 @@ public function create_item( $request ) {
}
}

// Sites can exclude emoji or limit reactions to the named list
// via `gutenberg_note_reaction_emoji_settings`.
if ( $is_hex_key ) {
$is_hex_key = gutenberg_is_note_reaction_hex_key_allowed( $emoji_key );
}

if ( ! $is_hex_key ) {
return new WP_Error(
'rest_comment_invalid_reaction',
Expand Down
48 changes: 45 additions & 3 deletions lib/compat/wordpress-7.2/emojibase-data.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
*/

/**
* Injects the Emojibase dataset URL into the block editor settings, where
* the Notes picker reads it. npm consumers of `@wordpress/editor` opt in by
* supplying the same setting.
* Injects the Emojibase dataset URL and per-emoji label overrides into the
* block editor settings, where the Notes picker reads them. npm consumers
* of `@wordpress/editor` opt in by supplying the same two settings.
*
* @since 7.2.0
*
Expand All @@ -25,6 +25,48 @@ function gutenberg_add_emojibase_settings( $settings ) {
if ( is_dir( gutenberg_dir_path() . 'build/emojibase-data' ) ) {
$settings['noteEmojibaseUrl'] = gutenberg_url( 'build/emojibase-data' );
}
$settings['noteEmojiLabelOverrides'] = gutenberg_get_emoji_picker_label_overrides();
return $settings;
}
add_filter( 'block_editor_settings_all', 'gutenberg_add_emojibase_settings' );

/**
* Builds the per-emoji label override map exposed to the editor's picker.
*
* Emojibase translates labels for 28 locales only; the filter below lets
* sites fill the gap for the emojis they care about. Seeded with the
* named reactions so an emoji keeps its named label in the picker.
*
* @since 7.2.0
*
* @return array Map of `hexcode => translated label`.
*/
function gutenberg_get_emoji_picker_label_overrides() {
$defaults = array();
if ( function_exists( 'gutenberg_get_note_reaction_emoji_settings' ) ) {
$emoji_settings = gutenberg_get_note_reaction_emoji_settings();
foreach ( $emoji_settings['emojis'] as $entry ) {
$defaults[ strtoupper( $entry['hexcode'] ) ] = $entry['label'];
}
}

/**
* Filters the emoji label overrides exposed to the Notes picker.
*
* Keys are uppercase Emojibase `hexcode` values: each code point
* zero-padded to four digits with U+FE0F stripped, e.g. `2764` for
* ❤️ and `00A9` for ©️.
*
* @since 7.2.0
*
* @param array $overrides Map of `hexcode => translated label`.
*/
$overrides = apply_filters(
'gutenberg_emoji_picker_label_overrides',
$defaults
);

// A non-string label would crash `override.toLowerCase()` in searchEmojis().
$overrides = is_array( $overrides ) ? $overrides : array();
return array_filter( $overrides, 'is_string' );
}
197 changes: 197 additions & 0 deletions lib/compat/wordpress-7.2/note-reactions.php
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,203 @@ function gutenberg_exclude_notes_from_comment_count_7_2( $new_count, $old_count,
}
add_filter( 'pre_wp_update_comment_count_now', 'gutenberg_exclude_notes_from_comment_count_7_2', 10, 3 );

/**
* Formats an emoji hex key in the reaction storage form: lowercase, each
* code point padded to four digits, U+FE0F stripped. Accepts Emojibase
* hexcodes (`2764-FE0F`) and stored keys.
*
* @since 7.2.0
*
* @param string $hex_key Hex code points joined by `-`.
* @return string The formatted hex key, or empty string when invalid.
*/
function gutenberg_format_note_reaction_hex_key( $hex_key ) {
if ( ! is_string( $hex_key ) || ! preg_match( '/^[0-9a-f]{1,6}(-[0-9a-f]{1,6})*$/i', $hex_key ) ) {
return '';
}
$parts = array();
foreach ( explode( '-', strtolower( $hex_key ) ) as $part ) {
if ( 0xFE0F === hexdec( $part ) ) {
continue;
}
$parts[] = str_pad( $part, 4, '0', STR_PAD_LEFT );
}
return implode( '-', $parts );
}

/**
* Reduces an emoji hex key to its base emoji: formatted as in
* gutenberg_format_note_reaction_hex_key(), with skin-tone modifiers
* stripped, so a rule for an emoji covers its skin-tone variants.
*
* @since 7.2.0
*
* @param string $hex_key Hex code points joined by `-`.
* @return string The base emoji's hex key, or empty string when invalid.
*/
function gutenberg_normalize_note_reaction_hex_key( $hex_key ) {
$formatted = gutenberg_format_note_reaction_hex_key( $hex_key );
if ( '' === $formatted ) {
return '';
}
$parts = array();
foreach ( explode( '-', $formatted ) as $part ) {
$value = hexdec( $part );
// The five Fitzpatrick skin-tone modifiers.
if ( $value >= 0x1F3FB && $value <= 0x1F3FF ) {
continue;
}
$parts[] = $part;
}
return implode( '-', $parts );
}

/**
* Returns the note reaction emoji settings: the named emoji list plus the
* rules for which other emoji the picker offers and the REST API accepts.
*
* @since 7.2.0
*
* @return array {
* @type array[] $emojis Named emoji, each with a `hexcode` key
* in storage form and a `label`.
* @type bool $allow_unlisted Whether emoji outside the named list are accepted.
* @type string[] $exclude Base hex keys that are never accepted.
* }
*/
function gutenberg_get_note_reaction_emoji_settings() {
$default_emojis = array(
array(
'hexcode' => '2764',
'label' => _x( 'heart', 'emoji reaction', 'gutenberg' ),
),
array(
'hexcode' => '1F389',
'label' => _x( 'celebration', 'emoji reaction', 'gutenberg' ),
),
array(
'hexcode' => '1F604',
'label' => _x( 'smile', 'emoji reaction', 'gutenberg' ),
),
array(
'hexcode' => '1F440',
'label' => _x( 'eyes', 'emoji reaction', 'gutenberg' ),
),
array(
'hexcode' => '1F680',
'label' => _x( 'rocket', 'emoji reaction', 'gutenberg' ),
),
);

/**
* Filters which emoji note reactions offer and accept.
*
* Named emoji seed the picker's "Frequently used" section, carry their
* own labels, and are always accepted. `allow_unlisted` and `exclude`
* apply to every other emoji in the picker.
*
* @since 7.2.0
*
* @param array $settings {
* @type array[] $emojis Named emoji. Each item has a `hexcode`
* (Emojibase form, such as `1F984`) and
* a `label`.
* @type bool $allow_unlisted Whether any emoji from the picker is
* accepted. False limits reactions to the
* named list. Default true.
* @type string[] $exclude Emojibase hexcodes to reject, such as
* `1F595`. Excluding an emoji also excludes
* its skin-tone variants. Default empty.
* }
*/
$defaults = array(
'emojis' => $default_emojis,
'allow_unlisted' => true,
'exclude' => array(),
);
$settings = apply_filters( 'gutenberg_note_reaction_emoji_settings', $defaults );
// Keys a callback leaves out keep their defaults.
$settings = is_array( $settings ) ? wp_parse_args( $settings, $defaults ) : $defaults;

$emojis = array();
if ( is_array( $settings['emojis'] ) ) {
foreach ( $settings['emojis'] as $entry ) {
if ( ! is_array( $entry ) || ! isset( $entry['hexcode'], $entry['label'] ) || ! is_string( $entry['label'] ) ) {
continue;
}
$hex_key = gutenberg_format_note_reaction_hex_key( $entry['hexcode'] );
if ( '' !== $hex_key ) {
$emojis[] = array(
'hexcode' => $hex_key,
'label' => $entry['label'],
);
}
}
}

$exclude = array();
if ( is_array( $settings['exclude'] ) ) {
foreach ( $settings['exclude'] as $hexcode ) {
$hex_key = gutenberg_normalize_note_reaction_hex_key( $hexcode );
if ( '' !== $hex_key ) {
$exclude[] = $hex_key;
}
}
}

return array(
'emojis' => $emojis,
'allow_unlisted' => (bool) $settings['allow_unlisted'],
'exclude' => array_values( array_unique( $exclude ) ),
);
}

/**
* Injects the note reaction emoji settings into the block editor settings
* so the picker offers the same set the REST API accepts.
*
* @since 7.2.0
*
* @param array $settings Existing block editor settings.
* @return array Updated block editor settings.
*/
function gutenberg_add_note_reaction_emojis_setting( $settings ) {
$emoji_settings = gutenberg_get_note_reaction_emoji_settings();

$settings['noteReactionEmojis'] = $emoji_settings['emojis'];
$settings['noteReactionEmojiRules'] = array(
'allowUnlisted' => $emoji_settings['allow_unlisted'],
'exclude' => $emoji_settings['exclude'],
);
return $settings;
}
add_filter( 'block_editor_settings_all', 'gutenberg_add_note_reaction_emojis_setting' );

/**
* Whether a reaction hex key is accepted under the emoji rules. An emoji
* from the named list is always accepted, including its skin-tone variants.
*
* @since 7.2.0
*
* @param string $hex_key The submitted hex key.
* @return bool Whether the reaction is allowed.
*/
function gutenberg_is_note_reaction_hex_key_allowed( $hex_key ) {
$base = gutenberg_normalize_note_reaction_hex_key( $hex_key );
if ( '' === $base ) {
return false;
}

$settings = gutenberg_get_note_reaction_emoji_settings();
foreach ( $settings['emojis'] as $entry ) {
if ( gutenberg_normalize_note_reaction_hex_key( $entry['hexcode'] ) === $base ) {
return true;
}
}

return $settings['allow_unlisted'] && ! in_array( $base, $settings['exclude'], true );
}

/**
* Returns the reaction children of a note.
*
Expand Down
31 changes: 31 additions & 0 deletions packages/e2e-tests/plugins/note-reaction-emojis.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php
/**
* Plugin Name: Gutenberg Test Note Reaction Emojis
* Plugin URI: https://github.com/WordPress/gutenberg
* Author: Gutenberg Team
*
* @package gutenberg-test-note-reaction-emojis
*/

/**
* Exercises the `gutenberg_note_reaction_emoji_settings` filter end-to-end:
* adds named emoji with their own labels, which the picker must offer and
* the REST API accept, and excludes one the picker must no longer offer.
*
* @param array $settings Default emoji settings.
* @return array Filtered emoji settings.
*/
function gutenberg_test_note_reaction_emojis( $settings ) {
$settings['emojis'][] = array(
'hexcode' => '1F984',
'label' => 'Unicorn',
);
$settings['emojis'][] = array(
'hexcode' => '1F3C6',
'label' => 'Trophy',
);
// Thumbs down, along with its skin-tone variants.
$settings['exclude'][] = '1F44E';
return $settings;
}
add_filter( 'gutenberg_note_reaction_emoji_settings', 'gutenberg_test_note_reaction_emojis' );
1 change: 1 addition & 0 deletions packages/editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

- Notes: allow reacting to a note with an emoji. Reactions display as pill buttons on the note with per-emoji counts, backed by a new `reaction` comment type. The add-reaction trigger shows on a selected note, fading in on hover or focus in its top corner, so a note with no reactions costs no vertical space for the option ([#76767](https://github.com/WordPress/gutenberg/pull/76767)).
- Notes: the add-reaction button now opens a full searchable emoji picker with a per-user skin tone preference, powered by a self-hosted Emojibase dataset configured via the `noteEmojibaseUrl` editor setting ([#78176](https://github.com/WordPress/gutenberg/pull/78176)).
- Notes: sites can customize emoji reactions through the `noteReactionEmojis`, `noteReactionEmojiRules`, and `noteEmojiLabelOverrides` editor settings, which the plugin fills from the `gutenberg_note_reaction_emoji_settings` and `gutenberg_emoji_picker_label_overrides` filters ([#84043](https://github.com/WordPress/gutenberg/pull/84043)).

### Enhancements

Expand Down
2 changes: 1 addition & 1 deletion packages/editor/src/components/collab-sidebar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ collab-sidebar/
│ ├── skin-tone-picker.tsx SkinTonePicker - default skin tone preference Menu
│ ├── emojibase-data.ts Emojibase dataset loading, labels, and settings
│ ├── frequent-emojis.ts useFrequentEmojis - persisted "Frequently used" section
│ └── reaction-emojis.ts curated reaction set and storage keys
│ └── reaction-emojis.ts curated reaction set, storage keys, and the site's emoji rules
│
├── hooks.js useNoteThreads, useNoteActions, useNoteSelection, usePickNote, useNoteFocus, useFloatingBoard, useEnableFloatingSidebar
├── utils.js focusNoteThread, getNoteExcerpt, sanitizeNoteContent, calculateNotePositions, getAvatarBorderColor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import { __ } from '@wordpress/i18n';
import { IconButton } from '@wordpress/ui';
import { reaction as reactionIcon } from '@wordpress/icons';
import EmojiPicker from './emoji-picker';
import { emojiToHexKey } from './reaction-emojis';
import {
emojiToHexKey,
useReactionEmojiRules,
useReactionEmojis,
} from './reaction-emojis';
import { hasUserReacted } from './reaction-display';
import type { ReactionSummary } from './reaction-display';

Expand All @@ -18,7 +22,7 @@ interface AddReactionButtonProps {

/**
* Standalone add-reaction button, opening the searchable emoji picker
* with its "Frequently used" section seeded from the curated set.
* with its "Frequently used" section seeded from the named set.
*
* @param props Component props.
* @param props.reactions The note's reaction summary, used to mark the
Expand All @@ -29,6 +33,15 @@ export function AddReactionButton( {
reactions,
onToggleReaction,
}: AddReactionButtonProps ) {
const emojis = useReactionEmojis();
const rules = useReactionEmojiRules();

// A site that empties the named list and allows nothing else leaves
// nothing to pick.
if ( ! emojis.length && ! rules.allowUnlisted ) {
return null;
}

const reactedHexKeys = Object.keys( reactions ?? {} ).filter( ( hexKey ) =>
hasUserReacted( reactions, hexKey )
);
Expand Down
Loading
Loading