diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 4011d44b61..a30a5b0113 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -3480,6 +3480,10 @@ "messageformat": "Recently Used", "description": "FunPicker > Stickers Panel > Sub Nav > Category Label > Recents" }, + "icu:FunPanelStickers__SubNavButton--AddStickerPack": { + "messageformat": "Add a sticker pack", + "description": "FunPicker > Stickers Panel > Sub Nav > Button > Add a sticker pack (accessibility label, opens sticker manager)" + }, "icu:FunPanelStickers__SectionTitle--SearchResults": { "messageformat": "Search Results", "description": "FunPicker > Stickers Panel > Section Title > Search Results" diff --git a/package.json b/package.json index d0a94a781b..8b35f59975 100644 --- a/package.json +++ b/package.json @@ -689,5 +689,8 @@ "!node_modules/.cache", "sticker-creator/dist/**" ] + }, + "volta": { + "node": "22.18.0" } } diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 338b97df58..f27bef85c8 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -6016,1040 +6016,6 @@ button.module-calling-participants-list__contact { outline: none; } -// Module: StickerPicker - -.module-sticker-picker { - @include mixins.module-composition-popper; - & { - height: 400px; - display: grid; - grid-template-rows: 44px 1fr; - grid-template-columns: 1fr; - z-index: variables.$z-index-context-menu; - } -} - -.module-sticker-picker__header { - display: flex; - flex-direction: row; - padding-block: 0; - padding-inline: 8px; - justify-content: flex-start; - align-items: center; -} - -.module-sticker-picker__header__packs { - width: 288px; - overflow: hidden; - position: relative; - - &__slider { - display: flex; - flex-direction: row; - /* stylelint-disable-next-line declaration-property-value-disallowed-list */ - transform: translateX(0); - transition: transform 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); - } -} - -.module-sticker-picker__recents--title { - color: variables.$color-gray-05; -} - -.module-sticker-picker__header__button { - width: 28px; - height: 28px; - border: 0; - border-radius: 8px; - display: flex; - justify-content: center; - align-items: center; - background: none; - margin-inline-end: 4px; - - outline: none; - - &:active, - &:focus { - @include mixins.keyboard-mode { - background: variables.$color-gray-05; - } - @include mixins.dark-keyboard-mode { - background: variables.$color-gray-60; - } - } - - &--selected { - @include mixins.light-theme { - background: variables.$color-gray-15; - } - @include mixins.dark-theme { - background: variables.$color-gray-45; - } - } - - &--recents, - &--add-pack { - &::after { - content: ''; - display: block; - min-width: 20px; - min-height: 20px; - } - } - - &--recents { - &::after { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/recent/recent.svg', - variables.$color-gray-60 - ); - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/recent/recent.svg', - variables.$color-gray-25 - ); - } - } - } - - &--add-pack { - &::after { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/plus/plus.svg', - variables.$color-gray-60 - ); - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/plus/plus.svg', - variables.$color-gray-25 - ); - } - } - } - - &--prev-page, - &--next-page { - top: 0; - margin: 0; - border-radius: 0; - - &::after { - content: ''; - display: block; - min-width: 16px; - min-height: 16px; - } - - @include mixins.light-theme { - background: variables.$color-gray-02; - } - - @include mixins.dark-theme { - background: variables.$color-gray-75; - } - } - - &--prev-page { - position: absolute; - inset-inline-start: 0; - - &:dir(ltr) { - &::after { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/chevron/chevron-left.svg', - variables.$color-gray-60 - ); - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/chevron/chevron-left.svg', - variables.$color-gray-25 - ); - } - } - } - &:dir(rtl) { - &::after { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/chevron/chevron-right.svg', - variables.$color-gray-60 - ); - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/chevron/chevron-right.svg', - variables.$color-gray-25 - ); - } - } - } - } - - &--next-page { - position: absolute; - inset-inline-end: 0; - - &:dir(ltr) { - &::after { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/chevron/chevron-right.svg', - variables.$color-gray-60 - ); - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/chevron/chevron-right.svg', - variables.$color-gray-25 - ); - } - } - } - &:dir(rtl) { - &::after { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/chevron/chevron-left.svg', - variables.$color-gray-60 - ); - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/chevron/chevron-left.svg', - variables.$color-gray-25 - ); - } - } - } - } - - &--error { - position: relative; - - &::before { - display: block; - content: ''; - width: 12px; - height: 12px; - position: absolute; - inset-inline-start: 14px; - top: 2px; - @include mixins.color-svg( - '../images/icons/v3/error/error-circle.svg', - variables.$color-accent-red - ); - } - } - - &--hint { - position: relative; - &::before { - display: block; - content: ''; - position: absolute; - top: 0; - inset-inline-end: 0; - width: 14px; - height: 14px; - border-radius: 7px; - background: variables.$color-ultramarine; - } - } -} - -.module-sticker-picker__header__button__image { - width: 20px; - height: 20px; - object-fit: contain; -} - -.module-sticker-picker__header__button__image--placeholder { - min-width: 20px; - min-height: 20px; - max-width: 20px; - max-height: 20px; - background-color: variables.$color-gray-05; -} - -.module-sticker-picker__body { - position: relative; - - &__grid { - display: grid; - grid-gap: 8px; - grid-template-columns: repeat(4, 1fr); - grid-auto-rows: 68px; - } - - &__content { - width: 332px; - height: 356px; - padding-block: 8px 16px; - padding-inline: 13px; - overflow-y: auto; - - &--under-text { - height: 320px; - } - - &--under-long-text { - height: 304px; - } - } - - &__cell { - border: none; - background: none; - padding: 0; - width: 68px; - height: 68px; - display: flex; - justify-content: center; - align-items: center; - - @include mixins.mouse-mode { - outline: none; - } - - &__image, - &__placeholder { - width: 100%; - height: 100%; - object-fit: contain; - } - - &__placeholder { - border-radius: 4px; - - @include mixins.light-theme() { - background-color: variables.$color-gray-05; - } - - @include mixins.dark-theme() { - background-color: variables.$color-gray-60; - } - } - } - - &--empty { - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - } - - &__text { - @include mixins.font-body-1-bold; - - text-align: center; - padding-block: 8px 12px; - padding-inline: 0 16px; - - @include mixins.light-theme() { - color: variables.$color-gray-60; - } - - @include mixins.dark-theme() { - color: variables.$color-gray-25; - } - - &:only-child { - padding-block: 0 28px; - padding-inline: 0; // header height to offset the text so it is centered in the whole picker - } - - &--error { - @include mixins.light-theme() { - color: variables.$color-accent-red; - } - @include mixins.dark-theme() { - color: variables.$color-accent-red; - } - } - - &--hint { - @include mixins.light-theme() { - color: variables.$color-ultramarine; - } - - @include mixins.dark-theme() { - color: variables.$color-ultramarine-light; - } - } - - &--pin { - padding-block: 8px 12px; - padding-inline: 0px 16px; - position: absolute; - top: 0; - } - } -} - -.module-sticker-picker__time--digital { - @include mixins.time-fonts; - color: variables.$color-white; - font-size: 28px; - line-height: 0px; -} - -.module-sticker-picker__time--analog { - background: url(../images/analog-time/Arabic.svg) center no-repeat; - background-size: contain; - height: 64px; - position: relative; - width: 64px; -} - -.module-sticker-picker__time--analog__hour { - background: url(../images/analog-time/Arabic-hour.svg) center no-repeat; - height: 14px; - inset-inline-start: 50%; - margin-inline-start: -1px; - margin-top: -14px; - position: absolute; - top: 50%; - transform-origin: 50% 100%; - width: 2px; -} - -.module-sticker-picker__time--analog__minute { - background: url(../images/analog-time/Arabic-minute.svg) center no-repeat; - height: 22px; - inset-inline-start: 50%; - margin-inline-start: -1px; - margin-top: -22px; - position: absolute; - top: 50%; - transform-origin: 50% 100%; - width: 2px; -} - -// Module: Sticker button (launches the sticker picker) - -.sticker-button-wrapper { - height: 36px; - display: flex; - justify-content: center; - align-items: center; - margin-inline-start: 6px; -} - -.module-sticker-button__button { - border: 0; - border-radius: 4px; - background: none; - width: 32px; - height: 32px; - display: flex; - justify-content: center; - align-items: center; - - @include mixins.keyboard-mode { - &:focus { - outline: 2px solid variables.$color-ultramarine; - } - } - - outline: none; - - &::after { - display: block; - content: ''; - width: 20px; - height: 20px; - flex-shrink: 0; - - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/sticker/sticker.svg', - variables.$color-gray-75 - ); - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/sticker/sticker.svg', - variables.$color-gray-15 - ); - } - } - - &--active { - @include mixins.light-theme() { - background: variables.$color-gray-05; - } - - @include mixins.dark-theme() { - background: variables.$color-gray-75; - } - - & { - opacity: 1; - } - } -} - -.module-sticker-button__tooltip { - @include mixins.button-reset; - - & { - height: 34px; - display: flex; - justify-content: center; - align-items: center; - padding-block: 7px; - padding-inline: 12px; - border-radius: 8px; - margin-bottom: 6px; - z-index: variables.$z-index-tooltip; - - @include mixins.light-theme { - background: variables.$color-white; - } - - @include mixins.dark-theme { - background: variables.$color-gray-75; - } - - & { - @include mixins.popper-shadow(); - } - } - - &__triangle { - position: absolute; - width: 0; - height: 0; - border-style: solid; - border-width: 8px 8px 0 8px; - - @include mixins.light-theme { - border-color: variables.$color-white transparent transparent transparent; - } - - @include mixins.dark-theme { - border-color: variables.$color-gray-75 transparent transparent transparent; - } - - &--top-end { - top: 34px; - } - - &--introduction { - top: 72px; - } - } - - &__image { - width: 20px; - height: 20px; - object-fit: contain; - } - &__image-placeholder { - width: 20px; - height: 20px; - background-color: variables.$color-gray-05; - } - - &__text { - margin-inline-start: 4px; - cursor: default; - - @include mixins.light-theme { - color: variables.$color-gray-90; - } - - @include mixins.dark-theme { - color: variables.$color-gray-05; - } - - &__title { - font-weight: bold; - } - } - - &--introduction { - width: 420px; - height: 72px; - display: flex; - flex-direction: row; - - &__image { - width: 52px; - height: 52px; - } - - &__meta { - flex-grow: 1; - padding-block: 0; - padding-inline: 12px; - display: flex; - flex-direction: column; - justify-content: center; - - @include mixins.light-theme { - color: variables.$color-gray-90; - } - - @include mixins.dark-theme { - color: variables.$color-gray-05; - } - - &__title { - margin: 0; - - @include mixins.font-body-1-bold; - height: 16px; - } - - &__subtitle { - margin-top: 3px; - height: 16px; - } - } - - &__close { - flex-shrink: 1; - height: 100%; - &__button { - width: 16px; - height: 16px; - border: none; - - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/x/x-compact.svg', - variables.$color-gray-60 - ); - } - - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/x/x-compact.svg', - variables.$color-gray-05 - ); - } - } - } - } -} - -// Module: Emoji Picker - -%module-emoji-picker--ribbon { - height: 44px; - display: flex; - flex-direction: row; - align-items: center; -} - -.module-emoji-picker { - @include mixins.module-composition-popper; - & { - height: 428px; - display: grid; - grid-template-rows: 44px 1fr; - grid-template-columns: 1fr; - } - - &__header { - @extend %module-emoji-picker--ribbon; - justify-content: space-between; - margin-block: 0; - margin-inline: 12px; - - &__search-field { - flex-grow: 1; - margin-inline-start: 8px; - position: relative; - - @include mixins.font-body-2; - - &::after { - display: block; - content: ''; - width: 16px; - height: 16px; - position: absolute; - inset-inline-start: 8px; - top: 6px; - - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/search/search-compact.svg', - variables.$color-gray-60 - ); - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/search/search-compact.svg', - variables.$color-gray-25 - ); - } - } - - &__input { - width: 100%; - height: 28px; - - @include mixins.font-body-1; - - line-height: 28px; - - border-radius: 17px; - border-width: 1px; - border-style: solid; - padding-block: 0; - padding-inline: 30px 8px; - - &:focus { - outline: none; - } - - @include mixins.light-theme { - background: variables.$color-white; - color: variables.$color-gray-90; - border-color: variables.$color-gray-60; - - &:focus { - border-color: variables.$color-ultramarine; - } - - &:placeholder { - color: variables.$color-gray-45; - } - } - - @include mixins.dark-theme { - border-color: variables.$color-gray-25; - background: variables.$color-gray-75; - color: variables.$color-gray-05; - - &:focus { - border-color: variables.$color-ultramarine; - } - - &:placeholder { - color: variables.$color-gray-45; - } - } - } - } - } - - &__footer { - @extend %module-emoji-picker--ribbon; - justify-content: center; - - &__skin-tones { - align-items: center; - display: flex; - flex-direction: row; - flex-grow: 1; - justify-content: center; - } - - &__settings-spacer { - width: 28px; - margin-inline-end: 12px; - } - } - - &__button { - width: 28px; - height: 28px; - border: none; - border-radius: 8px; - padding: 0; - display: flex; - justify-content: center; - align-items: center; - background: none; - - @include mixins.mouse-mode { - outline: none; - } - - &--footer { - &:not(:last-of-type) { - margin-inline-end: 4px; - } - } - - &--settings { - margin-inline-start: 12px; - border-radius: 100%; - - @include mixins.light-theme { - background: variables.$color-white; - box-shadow: 0px 0px 4px variables.$color-black-alpha-20; - } - - @include mixins.dark-theme { - background: variables.$color-gray-65; - } - - &::before { - display: block; - width: 20px; - height: 20px; - content: ''; - - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/settings/settings.svg', - variables.$color-gray-75 - ); - } - - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/settings/settings.svg', - variables.$color-gray-25 - ); - } - } - } - - &--selected { - @include mixins.light-theme { - background: variables.$color-gray-05; - } - - @include mixins.dark-theme { - background: variables.$color-gray-60; - } - } - - &--icon { - display: flex; - justify-content: center; - align-items: center; - - &::after { - display: block; - content: ''; - width: 20px; - height: 20px; - } - - &--search { - &::after { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/search/search.svg', - variables.$color-gray-60 - ); - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/search/search.svg', - variables.$color-gray-25 - ); - } - } - } - - &--close { - &::after { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/x/x.svg', - variables.$color-gray-60 - ); - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/x/x.svg', - variables.$color-gray-25 - ); - } - } - } - - &--recents { - &::after { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/recent/recent.svg', - variables.$color-gray-60 - ); - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/recent/recent.svg', - variables.$color-gray-25 - ); - } - } - } - - &--emoji { - &::after { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/emoji/emoji.svg', - variables.$color-gray-60 - ); - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/emoji/emoji.svg', - variables.$color-gray-25 - ); - } - } - } - - $categories: animal food activity travel object symbol flag; - - @each $cat in $categories { - &--#{$cat} { - &::after { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/emoji/emoji-#{$cat}.svg', - variables.$color-gray-60 - ); - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/emoji/emoji-#{$cat}.svg', - variables.$color-gray-25 - ); - } - } - } - } - } - } - - &__body { - padding-block: 8px 0; - padding-inline: 12px 16px; - outline: none; - - &__emoji-cell { - display: flex; - flex-direction: row; - justify-content: center; - align-items: flex-start; - } - - &--empty { - display: flex; - padding: 0; - justify-content: center; - align-items: center; - @include mixins.font-body-1; - - @include mixins.light-theme { - color: variables.$color-gray-60; - } - - @include mixins.dark-theme { - color: variables.$color-gray-25; - } - } - } -} - -// Module: EmojiButton - -.emoji-button-wrapper { - height: 36px; - display: flex; - justify-content: center; - align-items: center; - margin-block: 0; - margin-inline: 6px; - padding-top: 4px; -} - -.module-emoji-button__button { - border: 0; - border-radius: 4px; - background: none; - width: 32px; - height: 32px; - display: flex; - justify-content: center; - align-items: center; - - @include mixins.keyboard-mode { - &:focus { - outline: 2px solid variables.$color-ultramarine; - } - } - - outline: none; - - &::after { - display: block; - content: ''; - width: 20px; - height: 20px; - flex-shrink: 0; - - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/emoji/emoji.svg', - variables.$color-gray-75 - ); - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/emoji/emoji.svg', - variables.$color-gray-15 - ); - } - } - - &--profile-editor::after { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/emoji/emoji-plus.svg', - variables.$color-gray-75 - ); - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/emoji/emoji-plus.svg', - variables.$color-gray-15 - ); - } - } - - &--has-emoji { - opacity: 1; - - &::after { - display: none; - } - } - - &--active { - @include mixins.light-theme() { - background: variables.$color-gray-05; - } - - @include mixins.dark-theme() { - background: variables.$color-gray-75; - } - - & { - opacity: 1; - } - } -} - // Module: Last Seen Indicator .module-last-seen-indicator { diff --git a/ts/RemoteConfig.ts b/ts/RemoteConfig.ts index e3b740c197..7d8a870d11 100644 --- a/ts/RemoteConfig.ts +++ b/ts/RemoteConfig.ts @@ -39,9 +39,6 @@ const KnownConfigKeys = [ 'desktop.libsignalNet.shadowAuthChatWithNoise', 'desktop.libsignalNet.chatPermessageDeflate', 'desktop.libsignalNet.chatPermessageDeflate.prod', - 'desktop.funPicker', // alpha - 'desktop.funPicker.beta', - 'desktop.funPicker.prod', 'desktop.pollReceive.alpha', 'desktop.pollReceive.beta', 'desktop.pollReceive.prod', diff --git a/ts/components/CallManager.stories.tsx b/ts/components/CallManager.stories.tsx index be203536bb..b408dac085 100644 --- a/ts/components/CallManager.stories.tsx +++ b/ts/components/CallManager.stories.tsx @@ -121,7 +121,6 @@ const createProps = (storyProps: Partial = {}): PropsType => ({ blockClient: action('block-client'), cancelPresenting: action('cancel-presenting'), renderDeviceSelection: () =>
, - renderEmojiPicker: () => <>EmojiPicker, renderReactionPicker: () =>
, sendGroupCallRaiseHand: action('send-group-call-raise-hand'), sendGroupCallReaction: action('send-group-call-reaction'), diff --git a/ts/components/CallManager.tsx b/ts/components/CallManager.tsx index 6fe4ae4e2e..11a0486fd9 100644 --- a/ts/components/CallManager.tsx +++ b/ts/components/CallManager.tsx @@ -47,7 +47,6 @@ import type { LocalizerType } from '../types/Util.js'; import { missingCaseError } from '../util/missingCaseError.js'; import { CallingToastProvider } from './CallingToast.js'; import type { SmartReactionPicker } from '../state/smart/ReactionPicker.js'; -import type { Props as ReactionPickerProps } from './conversation/ReactionPicker.js'; import { createLogger } from '../logging/log.js'; import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall.js'; import { CallingAdhocCallInfo } from './CallingAdhocCallInfo.js'; @@ -154,7 +153,7 @@ export type PropsType = { toggleSelfViewExpanded: () => unknown; toggleSettings: () => void; pauseVoiceNotePlayer: () => void; -} & Pick; +}; type ActiveCallManagerPropsType = { activeCall: ActiveCallType; @@ -194,7 +193,6 @@ function ActiveCallManager({ me, openSystemPreferencesAction, renderDeviceSelection, - renderEmojiPicker, renderReactionPicker, removeClient, selectPresentingSource, @@ -478,7 +476,6 @@ function ActiveCallManager({ isCallLinkAdmin={isCallLinkAdmin} me={me} openSystemPreferencesAction={openSystemPreferencesAction} - renderEmojiPicker={renderEmojiPicker} renderReactionPicker={renderReactionPicker} sendGroupCallRaiseHand={sendGroupCallRaiseHand} sendGroupCallReaction={sendGroupCallReaction} @@ -571,7 +568,6 @@ export function CallManager({ playRingtone, removeClient, renderDeviceSelection, - renderEmojiPicker, renderReactionPicker, ringingCall, selectPresentingSource, @@ -689,7 +685,6 @@ export function CallManager({ pauseVoiceNotePlayer={pauseVoiceNotePlayer} removeClient={removeClient} renderDeviceSelection={renderDeviceSelection} - renderEmojiPicker={renderEmojiPicker} renderReactionPicker={renderReactionPicker} selectPresentingSource={selectPresentingSource} sendGroupCallRaiseHand={sendGroupCallRaiseHand} diff --git a/ts/components/CallScreen.stories.tsx b/ts/components/CallScreen.stories.tsx index bf8eaa6bb9..42418c3f7d 100644 --- a/ts/components/CallScreen.stories.tsx +++ b/ts/components/CallScreen.stories.tsx @@ -244,7 +244,6 @@ const createProps = ( serviceId: overrideProps.myAci ?? generateAci(), }), openSystemPreferencesAction: action('open-system-preferences-action'), - renderEmojiPicker: () => <>EmojiPicker, renderReactionPicker: () =>
, cancelPresenting: action('cancel-presenting'), sendGroupCallRaiseHand: action('send-group-call-raise-hand'), diff --git a/ts/components/CallScreen.tsx b/ts/components/CallScreen.tsx index 9f3b916237..351b2cde18 100644 --- a/ts/components/CallScreen.tsx +++ b/ts/components/CallScreen.tsx @@ -74,7 +74,6 @@ import { } from './CallingToast.js'; import { handleOutsideClick } from '../util/handleOutsideClick.js'; import { Spinner } from './Spinner.js'; -import type { Props as ReactionPickerProps } from './conversation/ReactionPicker.js'; import type { SmartReactionPicker } from '../state/smart/ReactionPicker.js'; import { CallingRaisedHandsList, @@ -87,11 +86,12 @@ import { } from './CallReactionBurst.js'; import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall.js'; import { assertDev, strictAssert } from '../util/assert.js'; -import { emojiToData } from './emoji/lib.js'; import { CallingPendingParticipants } from './CallingPendingParticipants.js'; import type { CallingImageDataCache } from './CallManager.js'; import { FunStaticEmoji } from './fun/FunEmoji.js'; import { + getEmojiParentByKey, + getEmojiParentKeyByVariantKey, getEmojiVariantByKey, getEmojiVariantKeyByValue, isEmojiVariantValue, @@ -143,7 +143,7 @@ export type PropsType = { toggleSettings: () => void; changeCallView: (mode: CallViewMode) => void; setLocalAudioRemoteMuted: SetMutedByType; -} & Pick; +}; export const isInSpeakerView = ( call: Pick | undefined @@ -214,7 +214,6 @@ export function CallScreen({ isCallLinkAdmin, me, openSystemPreferencesAction, - renderEmojiPicker, renderReactionPicker, setGroupCallVideoRequest, sendGroupCallRaiseHand, @@ -1081,7 +1080,6 @@ export function CallScreen({ value: emoji, }); }, - renderEmojiPicker, })}
)} @@ -1336,8 +1334,9 @@ function useReactionsToast(props: UseReactionsToastType): void { ); // Normalize skin tone emoji to calculate burst threshold, but save original // value to show in the burst animation - const emojiData = emojiToData(value); - const normalizedValue = emojiData?.unified ?? value; + const emojiParentKey = getEmojiParentKeyByVariantKey(emojiVariantKey); + const emojiParent = getEmojiParentByKey(emojiParentKey); + const normalizedValue = emojiParent.value; reactionsShown.current.set(key, { value: normalizedValue, originalValue: value, diff --git a/ts/components/CompositionArea.stories.tsx b/ts/components/CompositionArea.stories.tsx index 4fc142a48f..a627f433ef 100644 --- a/ts/components/CompositionArea.stories.tsx +++ b/ts/components/CompositionArea.stories.tsx @@ -83,23 +83,11 @@ export default { draftText: undefined, getPreferredBadge: () => undefined, sortedGroupMembers: [], - // EmojiButton - onPickEmoji: action('onPickEmoji'), - onEmojiSkinToneDefaultChange: action('onEmojiSkinToneDefaultChange'), - recentEmojis: [], + // FunPicker + onSelectEmoji: action('onSelectEmoji'), emojiSkinToneDefault: EmojiSkinTone.Type1, - // StickerButton - knownPacks: [], - receivedPacks: [], - installedPacks: [], - blessedPacks: [], - recentStickers: [], - clearInstalledStickerPack: action('clearInstalledStickerPack'), pushPanelForConversation: action('pushPanelForConversation'), sendStickerMessage: action('sendStickerMessage'), - clearShowIntroduction: action('clearShowIntroduction'), - showPickerHint: false, - clearShowPickerHint: action('clearShowPickerHint'), // Message Requests conversationType: 'direct', acceptConversation: action('acceptConversation'), @@ -118,7 +106,7 @@ export default { showConversation: action('showConversation'), isSmsOnlyOrUnregistered: false, isFetchingUUID: false, - renderSmartCompositionRecording: _ =>
RECORDING
, + renderSmartCompositionRecording: () =>
RECORDING
, renderSmartCompositionRecordingDraft: _ =>
RECORDING DRAFT
, // Select mode selectedMessageIds: undefined, @@ -149,14 +137,7 @@ export function StartingText(args: Props): JSX.Element { export function StickerButton(args: Props): JSX.Element { const theme = useContext(StorybookThemeContext); - return ( - - ); + return ; } export function MessageRequest(args: Props): JSX.Element { diff --git a/ts/components/CompositionArea.tsx b/ts/components/CompositionArea.tsx index 6c7a0fd349..d5720adfcd 100644 --- a/ts/components/CompositionArea.tsx +++ b/ts/components/CompositionArea.tsx @@ -14,13 +14,6 @@ import { RecordingState } from '../types/AudioRecorder.js'; import type { imageToBlurHash } from '../util/imageToBlurHash.js'; import { dropNull } from '../util/dropNull.js'; import { Spinner } from './Spinner.js'; -import type { - Props as EmojiButtonProps, - EmojiButtonAPI, -} from './emoji/EmojiButton.js'; -import { EmojiButton } from './emoji/EmojiButton.js'; -import type { Props as StickerButtonProps } from './stickers/StickerButton.js'; -import { StickerButton } from './stickers/StickerButton.js'; import type { InputApi, Props as CompositionInputProps, @@ -48,7 +41,6 @@ import type { PushPanelForConversationActionType, ShowConversationType, } from '../state/ducks/conversations.js'; -import type { EmojiPickDataType } from './emoji/EmojiPicker.js'; import type { LinkPreviewForUIType } from '../types/message/LinkPreviews.js'; import { isSameLinkPreview } from '../types/message/LinkPreviews.js'; @@ -56,7 +48,6 @@ import { MandatoryProfileSharingActions } from './conversation/MandatoryProfileS import { MediaQualitySelector } from './MediaQualitySelector.js'; import type { Props as QuoteProps } from './conversation/Quote.js'; import { Quote } from './conversation/Quote.js'; -import { countStickers } from './stickers/lib.js'; import { useAttachFileShortcut, useEditLastMessageSent, @@ -69,7 +60,6 @@ import { usePrevious } from '../hooks/usePrevious.js'; import { PanelType } from '../types/Panels.js'; import type { SmartCompositionRecordingDraftProps } from '../state/smart/CompositionRecordingDraft.js'; import { useEscapeHandling } from '../hooks/useEscapeHandling.js'; -import type { SmartCompositionRecordingProps } from '../state/smart/CompositionRecording.js'; import SelectModeActions from './conversation/SelectModeActions.js'; import type { ShowToastAction } from '../state/ducks/toast.js'; import type { DraftEditMessageType } from '../model-types.d.ts'; @@ -84,9 +74,7 @@ import type { SmartDraftGifMessageSendModalProps } from '../state/smart/DraftGif import { strictAssert } from '../util/assert.js'; import { ConfirmationDialog } from './ConfirmationDialog.js'; import type { EmojiSkinTone } from './fun/data/emojis.js'; -import type { StickerPackType, StickerType } from '../state/ducks/stickers.js'; import { FunPickerButton } from './fun/FunButton.js'; -import { isFunPickerEnabled } from './fun/isFunPickerEnabled.js'; export type OwnProps = Readonly<{ acceptedMessageRequest: boolean | null; @@ -195,9 +183,7 @@ export type OwnProps = Readonly<{ showConversation: ShowConversationType; startRecording: (id: string) => unknown; theme: ThemeType; - renderSmartCompositionRecording: ( - props: SmartCompositionRecordingProps - ) => JSX.Element; + renderSmartCompositionRecording: () => JSX.Element; renderSmartCompositionRecordingDraft: ( props: SmartCompositionRecordingDraftProps ) => JSX.Element | null; @@ -212,11 +198,8 @@ export type OwnProps = Readonly<{ props: SmartDraftGifMessageSendModalProps | null ) => void; - onPickEmoji: (e: EmojiPickDataType) => void; + onSelectEmoji: (emojiSelection: FunEmojiSelection) => void; emojiSkinToneDefault: EmojiSkinTone | null; - // StickerButton - installedPacks: ReadonlyArray; - recentStickers: ReadonlyArray; }>; export type Props = Pick< @@ -231,27 +214,6 @@ export type Props = Pick< | 'sendCounter' | 'sortedGroupMembers' > & - Pick< - EmojiButtonProps, - | 'onPickEmoji' - | 'onEmojiSkinToneDefaultChange' - | 'recentEmojis' - | 'emojiSkinToneDefault' - > & - Pick< - StickerButtonProps, - | 'knownPacks' - | 'receivedPacks' - | 'installedPack' - | 'installedPacks' - | 'blessedPacks' - | 'recentStickers' - | 'clearInstalledStickerPack' - | 'showIntroduction' - | 'clearShowIntroduction' - | 'showPickerHint' - | 'clearShowPickerHint' - > & MessageRequestActionsProps & Pick & Pick & { @@ -317,24 +279,10 @@ export const CompositionArea = memo(function CompositionArea({ ourConversationId, sendCounter, sortedGroupMembers, - // EmojiButton - onPickEmoji, - onEmojiSkinToneDefaultChange, - recentEmojis, + // FunPicker + onSelectEmoji, emojiSkinToneDefault, - // StickerButton - knownPacks, - receivedPacks, - installedPack, - installedPacks, - blessedPacks, - recentStickers, - clearInstalledStickerPack, sendStickerMessage, - showIntroduction, - clearShowIntroduction, - showPickerHint, - clearShowPickerHint, // Message Requests acceptedMessageRequest, areWePending, @@ -383,7 +331,6 @@ export const CompositionArea = memo(function CompositionArea({ AttachmentDraftType | undefined >(); const inputApiRef = useRef(); - const emojiButtonRef = useRef(); const fileInputRef = useRef(null); const handleForceSend = useCallback(() => { @@ -424,8 +371,6 @@ export const CompositionArea = memo(function CompositionArea({ return false; } - emojiButtonRef.current?.close(); - if (editedMessageId) { sendEditedMessage(conversationId, { bodyRanges, @@ -524,14 +469,6 @@ export const CompositionArea = memo(function CompositionArea({ } }, [inputApiRef, focusCounter, previousFocusCounter]); - const withStickers = - countStickers({ - knownPacks, - blessedPacks, - installedPacks, - receivedPacks, - }) > 0; - const previousMessageCompositionId = usePrevious( messageCompositionId, messageCompositionId @@ -554,16 +491,6 @@ export const CompositionArea = memo(function CompositionArea({ previousSendCounter, ]); - const insertEmoji = useCallback( - (e: EmojiPickDataType) => { - if (inputApiRef.current) { - inputApiRef.current.insertEmoji(e); - onPickEmoji(e); - } - }, - [inputApiRef, onPickEmoji] - ); - // We want to reset the state of Quill only if: // // - Our other device edits the message (edit history length would change) @@ -627,12 +554,11 @@ export const CompositionArea = memo(function CompositionArea({ const handleFunPickerSelectEmoji = useCallback( (emojiSelection: FunEmojiSelection) => { - insertEmoji({ - shortName: emojiSelection.englishShortName, - skinTone: emojiSelection.skinTone, - }); + if (inputApiRef.current) { + inputApiRef.current.insertEmoji(emojiSelection); + } }, - [insertEmoji] + [] ); const handleFunPickerSelectSticker = useCallback( (stickerSelection: FunStickerSelection) => { @@ -720,34 +646,19 @@ export const CompositionArea = memo(function CompositionArea({ {i18n('icu:CompositionArea__ConfirmGifSelection__Body')} )} - {isFunPickerEnabled() && ( -
- - - -
- )} - {!isFunPickerEnabled() && ( -
- setComposerFocus(conversationId)} - recentEmojis={recentEmojis} - emojiSkinToneDefault={emojiSkinToneDefault} - onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange} - /> -
- )} +
+ + + +
{showMediaQualitySelector ? (
) : null; - const stickerButtonPlacement = large ? 'top-start' : 'top-end'; - const stickerButtonFragment = - !isFunPickerEnabled() && !draftEditMessage && withStickers ? ( -
- - pushPanelForConversation({ - type: PanelType.StickerManager, - }) - } - onPickSticker={(packId, stickerId) => - sendStickerMessage(conversationId, { packId, stickerId }) - } - showIntroduction={showIntroduction} - clearShowIntroduction={clearShowIntroduction} - showPickerHint={showPickerHint} - clearShowPickerHint={clearShowPickerHint} - position={stickerButtonPlacement} - /> -
- ) : null; - // Listen for cmd/ctrl-shift-x to toggle large composition mode useEffect(() => { const handler = (e: KeyboardEvent) => { @@ -876,10 +757,6 @@ export const CompositionArea = memo(function CompositionArea({ }; }, [platform, setLarge]); - const handleRecordingBeforeSend = useCallback(() => { - emojiButtonRef.current?.close(); - }, [emojiButtonRef]); - const handleEscape = useCallback(() => { if (linkPreviewResult) { onCloseLinkPreview(conversationId); @@ -1060,9 +937,7 @@ export const CompositionArea = memo(function CompositionArea({ } if (isRecording) { - return renderSmartCompositionRecording({ - onBeforeSend: handleRecordingBeforeSend, - }); + return renderSmartCompositionRecording(); } if (draftAttachments.length === 1 && isVoiceMessage(draftAttachments[0])) { @@ -1085,7 +960,6 @@ export const CompositionArea = memo(function CompositionArea({ i18n={i18n} imageSrc={attachmentToEdit.url} imageToBlurHash={imageToBlurHash} - installedPacks={installedPacks} isCreatingStory={false} isFormattingEnabled={isFormattingEnabled} isSending={false} @@ -1120,11 +994,10 @@ export const CompositionArea = memo(function CompositionArea({ true ); }} - onPickEmoji={onPickEmoji} + onSelectEmoji={onSelectEmoji} onTextTooLong={onTextTooLong} ourConversationId={ourConversationId} platform={platform} - recentStickers={recentStickers} emojiSkinToneDefault={emojiSkinToneDefault} sortedGroupMembers={sortedGroupMembers} /> @@ -1213,7 +1086,7 @@ export const CompositionArea = memo(function CompositionArea({ onCloseLinkPreview={onCloseLinkPreview} onDirtyChange={setDirty} onEditorStateChange={onEditorStateChange} - onPickEmoji={onPickEmoji} + onSelectEmoji={onSelectEmoji} onSubmit={handleSubmit} onTextTooLong={onTextTooLong} ourConversationId={ourConversationId} @@ -1228,7 +1101,6 @@ export const CompositionArea = memo(function CompositionArea({
{!large ? ( <> - {stickerButtonFragment} {!dirty ? micButtonFragment : null} {editMessageFragment} {attButton} @@ -1243,7 +1115,6 @@ export const CompositionArea = memo(function CompositionArea({ )} > {leftHandSideButtonsFragment} - {stickerButtonFragment} {attButton} {!dirty ? micButtonFragment : null} {editMessageFragment} diff --git a/ts/components/CompositionInput.stories.tsx b/ts/components/CompositionInput.stories.tsx index 355cf12cb6..17393ce92c 100644 --- a/ts/components/CompositionInput.stories.tsx +++ b/ts/components/CompositionInput.stories.tsx @@ -39,7 +39,7 @@ const useProps = (overrideProps: Partial = {}): Props => { large: overrideProps.large ?? false, onCloseLinkPreview: action('onCloseLinkPreview'), onEditorStateChange: action('onEditorStateChange'), - onPickEmoji: action('onPickEmoji'), + onSelectEmoji: action('onSelectEmoji'), onSubmit: action('onSubmit'), onTextTooLong: action('onTextTooLong'), ourConversationId: 'me', diff --git a/ts/components/CompositionInput.tsx b/ts/components/CompositionInput.tsx index c7d5e227c3..de3144f898 100644 --- a/ts/components/CompositionInput.tsx +++ b/ts/components/CompositionInput.tsx @@ -23,8 +23,6 @@ import { import { MonospaceBlot } from '../quill/formatting/monospaceBlot.js'; import { SpoilerBlot } from '../quill/formatting/spoilerBlot.js'; import { EmojiBlot, EmojiCompletion } from '../quill/emoji/index.js'; -import type { EmojiPickDataType } from './emoji/EmojiPicker.js'; -import { convertShortName } from './emoji/lib.js'; import type { DraftBodyRanges, HydratedBodyRangesType, @@ -79,12 +77,13 @@ import type { AutoSubstituteAsciiEmojisOptions } from '../quill/auto-substitute- import { AutoSubstituteAsciiEmojis } from '../quill/auto-substitute-ascii-emojis/index.js'; import { dropNull } from '../util/dropNull.js'; import { SimpleQuillWrapper } from './SimpleQuillWrapper.js'; -import type { EmojiSkinTone } from './fun/data/emojis.js'; +import { getEmojiVariantByKey, type EmojiSkinTone } from './fun/data/emojis.js'; import { FUN_STATIC_EMOJI_CLASS } from './fun/FunEmoji.js'; import { useFunEmojiSearch } from './fun/useFunEmojiSearch.js'; import type { EmojiCompletionOptions } from '../quill/emoji/completion.js'; import { useFunEmojiLocalizer } from './fun/useFunEmojiLocalizer.js'; import { MAX_BODY_ATTACHMENT_BYTE_LENGTH } from '../util/longAttachment.js'; +import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.js'; const log = createLogger('CompositionInput'); @@ -106,7 +105,7 @@ Quill.register( export type InputApi = { focus: () => void; - insertEmoji: (e: EmojiPickDataType) => void; + insertEmoji: (emojiSelection: FunEmojiSelection) => void; setContents: ( text: string, draftBodyRanges?: HydratedBodyRangesType, @@ -144,7 +143,7 @@ export type Props = Readonly<{ sendCounter: number; }): unknown; onTextTooLong(): unknown; - onPickEmoji(o: EmojiPickDataType): unknown; + onSelectEmoji: (emojiSelection: FunEmojiSelection) => void; onBlur?: () => unknown; onFocus?: () => unknown; onSubmit( @@ -184,7 +183,7 @@ export function CompositionInput(props: Props): React.ReactElement { onCloseLinkPreview, onBlur, onFocus, - onPickEmoji, + onSelectEmoji, onScroll, onSubmit, ourConversationId, @@ -285,7 +284,7 @@ export function CompositionInput(props: Props): React.ReactElement { quill.focus(); }; - const insertEmoji = (e: EmojiPickDataType) => { + const insertEmoji = (emojiSelection: FunEmojiSelection) => { const quill = quillRef.current; if (quill === undefined) { @@ -299,12 +298,12 @@ export function CompositionInput(props: Props): React.ReactElement { return; } - const emoji = convertShortName(e.shortName, e.skinTone); + const emojiVariant = getEmojiVariantByKey(emojiSelection.variantKey); const delta = new Delta() .retain(insertionRange.index) .delete(insertionRange.length) - .insert({ emoji: { value: emoji } }); + .insert({ emoji: { value: emojiVariant.value } }); quill.updateContents(delta, 'user'); quill.setSelection(insertionRange.index + 1, 0, 'user'); @@ -768,7 +767,7 @@ export function CompositionInput(props: Props): React.ReactElement { onChange, onEnter, onEscape, - onPickEmoji, + onSelectEmoji, onShortKeyEnter, onTab, }; @@ -842,8 +841,8 @@ export function CompositionInput(props: Props): React.ReactElement { }, emojiCompletion: { setEmojiPickerElement: setEmojiCompletionElement, - onPickEmoji: (emoji: EmojiPickDataType) => - callbacksRef.current.onPickEmoji(emoji), + onSelectEmoji: (emojiSelection: FunEmojiSelection) => + callbacksRef.current.onSelectEmoji(emojiSelection), emojiSkinToneDefault, emojiSearch, emojiLocalizer, diff --git a/ts/components/CompositionTextArea.tsx b/ts/components/CompositionTextArea.tsx index ddfedbb168..2fb10688ef 100644 --- a/ts/components/CompositionTextArea.tsx +++ b/ts/components/CompositionTextArea.tsx @@ -3,24 +3,20 @@ import React, { useRef, useCallback, useState } from 'react'; import type { LocalizerType } from '../types/I18N.js'; -import type { EmojiPickDataType } from './emoji/EmojiPicker.js'; import type { InputApi } from './CompositionInput.js'; import { CompositionInput } from './CompositionInput.js'; -import { EmojiButton } from './emoji/EmojiButton.js'; import { hydrateRanges, type DraftBodyRanges, type HydratedBodyRangesType, } from '../types/BodyRange.js'; import type { ThemeType } from '../types/Util.js'; -import type { Props as EmojiButtonProps } from './emoji/EmojiButton.js'; import type { PreferredBadgeSelectorType } from '../state/selectors/badges.js'; import * as grapheme from '../util/grapheme.js'; import { FunEmojiPicker } from './fun/FunEmojiPicker.js'; import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.js'; import type { EmojiSkinTone } from './fun/data/emojis.js'; import { FunEmojiPickerButton } from './fun/FunButton.js'; -import { isFunPickerEnabled } from './fun/isFunPickerEnabled.js'; import type { GetConversationByIdType } from '../state/selectors/conversations.js'; export type CompositionTextAreaProps = { @@ -32,12 +28,13 @@ export type CompositionTextAreaProps = { placeholder?: string; whenToShowRemainingCount?: number; onScroll?: (ev: React.UIEvent) => void; - onPickEmoji: (e: EmojiPickDataType) => void; + onSelectEmoji: (emojiSelection: FunEmojiSelection) => void; onChange: ( messageText: string, draftBodyRanges: HydratedBodyRangesType, caretLocation?: number | undefined ) => void; + emojiSkinToneDefault: EmojiSkinTone; onEmojiSkinToneDefaultChange: (emojiSkinToneDefault: EmojiSkinTone) => void; onSubmit: ( message: string, @@ -51,7 +48,7 @@ export type CompositionTextAreaProps = { draftText: string; theme: ThemeType; conversationSelector: GetConversationByIdType; -} & Pick; +}; /** * Essentially an HTML textarea but with support for emoji picker and @@ -69,15 +66,13 @@ export function CompositionTextArea({ isFormattingEnabled, maxLength, onChange, - onPickEmoji, + onSelectEmoji, onScroll, - onEmojiSkinToneDefaultChange, onSubmit, onTextTooLong, ourConversationId, placeholder, platform, - recentEmojis, emojiSkinToneDefault, theme, whenToShowRemainingCount = Infinity, @@ -88,25 +83,14 @@ export function CompositionTextArea({ grapheme.count(draftText) ); - const insertEmoji = useCallback( - (e: EmojiPickDataType) => { - if (inputApiRef.current) { - inputApiRef.current.insertEmoji(e); - onPickEmoji(e); - } - }, - [inputApiRef, onPickEmoji] - ); - const handleSelectEmoji = useCallback( (emojiSelection: FunEmojiSelection) => { - const data: EmojiPickDataType = { - shortName: emojiSelection.englishShortName, - skinTone: emojiSelection.skinTone, - }; - insertEmoji(data); + if (inputApiRef.current) { + inputApiRef.current.insertEmoji(emojiSelection); + onSelectEmoji(emojiSelection); + } }, - [insertEmoji] + [onSelectEmoji] ); const focusTextEditInput = useCallback(() => { @@ -182,7 +166,7 @@ export function CompositionTextArea({ large={false} moduleClassName="CompositionTextArea__input" onEditorStateChange={handleChange} - onPickEmoji={onPickEmoji} + onSelectEmoji={onSelectEmoji} onScroll={onScroll} onSubmit={onSubmit} onTextTooLong={onTextTooLong} @@ -205,27 +189,15 @@ export function CompositionTextArea({ shouldHidePopovers={null} />
- {!isFunPickerEnabled() && ( - - )} - {isFunPickerEnabled() && ( - - - - )} + + +
{maxLength !== undefined && characterCount >= whenToShowRemainingCount && ( diff --git a/ts/components/CustomizingPreferredReactionsModal.tsx b/ts/components/CustomizingPreferredReactionsModal.tsx index 94fc6fe456..61d8e111b8 100644 --- a/ts/components/CustomizingPreferredReactionsModal.tsx +++ b/ts/components/CustomizingPreferredReactionsModal.tsx @@ -1,8 +1,7 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React, { useState, useEffect, useCallback, useRef } from 'react'; -import { usePopper } from 'react-popper'; +import React, { useState, useCallback, useRef } from 'react'; import lodash from 'lodash'; import type { LocalizerType } from '../types/Util.js'; @@ -13,17 +12,16 @@ import { ReactionPickerPickerEmojiButton, ReactionPickerPickerStyle, } from './ReactionPickerPicker.js'; -import { EmojiPicker } from './emoji/EmojiPicker.js'; -import { DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES } from '../reactions/constants.js'; -import { convertShortName } from './emoji/lib.js'; -import { offsetDistanceModifier } from '../util/popperUtil.js'; -import { handleOutsideClick } from '../util/handleOutsideClick.js'; -import { EmojiSkinTone, getEmojiVariantByKey } from './fun/data/emojis.js'; +import { DEFAULT_PREFERRED_REACTION_EMOJI_PARENT_KEYS } from '../reactions/constants.js'; +import { + EmojiSkinTone, + getEmojiVariantByKey, + getEmojiVariantByParentKeyAndSkinTone, +} from './fun/data/emojis.js'; import { FunEmojiPicker } from './fun/FunEmojiPicker.js'; import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.js'; -import { isFunPickerEnabled } from './fun/isFunPickerEnabled.js'; -const { isEqual, noop } = lodash; +const { isEqual } = lodash; export type PropsType = { draftPreferredReactions: ReadonlyArray; @@ -52,57 +50,17 @@ export function CustomizingPreferredReactionsModal({ hadSaveError, i18n, isSaving, - onEmojiSkinToneDefaultChange, originalPreferredReactions, - recentEmojis, replaceSelectedDraftEmoji, resetDraftEmoji, savePreferredReactions, selectDraftEmojiToBeReplaced, selectedDraftEmojiIndex, }: Readonly): JSX.Element { - const [referenceElement, setReferenceElement] = - useState(null); const pickerRef = useRef(null); - const [popperElement, setPopperElement] = useState( - null - ); - const emojiPickerPopper = usePopper(referenceElement, popperElement, { - placement: 'bottom', - modifiers: [ - offsetDistanceModifier(8), - { - name: 'preventOverflow', - options: { altAxis: true }, - }, - ], - }); const isSomethingSelected = selectedDraftEmojiIndex !== undefined; - useEffect(() => { - if (!isSomethingSelected) { - return noop; - } - - return handleOutsideClick( - target => { - if ( - target instanceof Element && - target.closest('[data-fun-overlay]') != null - ) { - return true; - } - deselectDraftEmoji(); - return true; - }, - { - containerElements: [popperElement, pickerRef], - name: 'CustomizingPreferredReactionsModal.draftEmoji', - } - ); - }, [isSomethingSelected, popperElement, deselectDraftEmoji]); - const hasChanged = !isEqual( originalPreferredReactions, draftPreferredReactions @@ -110,9 +68,13 @@ export function CustomizingPreferredReactionsModal({ const canReset = !isSaving && !isEqual( - DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES.map(shortName => - convertShortName(shortName, emojiSkinToneDefault ?? EmojiSkinTone.None) - ), + DEFAULT_PREFERRED_REACTION_EMOJI_PARENT_KEYS.map(parentKey => { + const variant = getEmojiVariantByParentKeyAndSkinTone( + parentKey, + emojiSkinToneDefault ?? EmojiSkinTone.None + ); + return variant.value; + }), draftPreferredReactions ); const canSave = !isSaving && hasChanged; @@ -168,7 +130,6 @@ export function CustomizingPreferredReactionsModal({ {draftPreferredReactions.map((emoji, index) => { return ( @@ -199,31 +160,6 @@ export function CustomizingPreferredReactionsModal({ ? i18n('icu:CustomizingPreferredReactions__had-save-error') : i18n('icu:CustomizingPreferredReactions__subtitle')}
- {!isFunPickerEnabled() && isSomethingSelected && ( -
- { - const emoji = convertShortName( - pickedEmoji.shortName, - pickedEmoji.skinTone - ); - replaceSelectedDraftEmoji(emoji); - }} - recentEmojis={recentEmojis} - emojiSkinToneDefault={emojiSkinToneDefault} - onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange} - onClose={() => { - deselectDraftEmoji(); - }} - wasInvokedFromKeyboard={false} - /> -
- )} ); } @@ -249,26 +185,19 @@ function CustomizingPreferredReactionsModalItem(props: { [onDeselect] ); - const button = ( - + return ( + + + ); - - if (isFunPickerEnabled()) { - return ( - - {button} - - ); - } - return button; } diff --git a/ts/components/DraftGifMessageSendModal.stories.tsx b/ts/components/DraftGifMessageSendModal.stories.tsx index 44cf393cb7..f6ec3364d8 100644 --- a/ts/components/DraftGifMessageSendModal.stories.tsx +++ b/ts/components/DraftGifMessageSendModal.stories.tsx @@ -33,7 +33,7 @@ function RenderCompositionTextArea(props: SmartCompositionTextAreaProps) { i18n={i18n} isActive isFormattingEnabled - onPickEmoji={action('onPickEmoji')} + onSelectEmoji={action('onSelectEmoji')} onEmojiSkinToneDefaultChange={action('onEmojiSkinToneDefaultChange')} onTextTooLong={action('onTextTooLong')} ourConversationId="me" diff --git a/ts/components/ForwardMessagesModal.stories.tsx b/ts/components/ForwardMessagesModal.stories.tsx index e3b4006278..fbb11935bf 100644 --- a/ts/components/ForwardMessagesModal.stories.tsx +++ b/ts/components/ForwardMessagesModal.stories.tsx @@ -64,7 +64,7 @@ const useProps = (overrideProps: Partial = {}): PropsType => ({ i18n={i18n} isActive isFormattingEnabled - onPickEmoji={action('onPickEmoji')} + onSelectEmoji={action('onSelectEmoji')} onEmojiSkinToneDefaultChange={action('onEmojiSkinToneDefaultChange')} onTextTooLong={action('onTextTooLong')} ourConversationId="me" diff --git a/ts/components/MediaEditor.stories.tsx b/ts/components/MediaEditor.stories.tsx index bf09b51903..3fb6efd2f8 100644 --- a/ts/components/MediaEditor.stories.tsx +++ b/ts/components/MediaEditor.stories.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { action } from '@storybook/addon-actions'; import type { PropsType } from './MediaEditor.js'; import { MediaEditor } from './MediaEditor.js'; -import { Stickers, installedPacks } from '../test-helpers/getStickerPacks.js'; import { EmojiSkinTone } from './fun/data/emojis.js'; const { i18n } = window.SignalContext; @@ -24,15 +23,13 @@ export default { i18n, imageToBlurHash: input => Promise.resolve(input.toString()), imageSrc: IMAGE_2, - installedPacks, isFormattingEnabled: true, isSending: false, onClose: action('onClose'), onDone: action('onDone'), - onPickEmoji: action('onPickEmoji'), + onSelectEmoji: action('onSelectEmoji'), onTextTooLong: action('onTextTooLong'), platform: 'darwin', - recentStickers: [Stickers.wide, Stickers.tall, Stickers.abe], emojiSkinToneDefault: EmojiSkinTone.None, }, } satisfies Meta; diff --git a/ts/components/MediaEditor.tsx b/ts/components/MediaEditor.tsx index 6f84b61645..7ccbdd5688 100644 --- a/ts/components/MediaEditor.tsx +++ b/ts/components/MediaEditor.tsx @@ -14,10 +14,6 @@ import { createPortal } from 'react-dom'; import { fabric } from 'fabric'; import { useSelector } from 'react-redux'; import lodash from 'lodash'; -import type { - EmojiPickDataType, - Props as EmojiPickerProps, -} from './emoji/EmojiPicker.js'; import type { DraftBodyRanges } from '../types/BodyRange.js'; import type { ImageStateType } from '../mediaEditor/ImageStateType.js'; import type { @@ -26,7 +22,6 @@ import type { } from './CompositionInput.js'; import type { LocalizerType } from '../types/Util.js'; import type { MIMEType } from '../types/MIME.js'; -import type { Props as StickerButtonProps } from './stickers/StickerButton.js'; import type { imageToBlurHash } from '../util/imageToBlurHash.js'; import { MediaEditorFabricAnalogTimeSticker } from '../mediaEditor/MediaEditorFabricAnalogTimeSticker.js'; import { MediaEditorFabricCropRect } from '../mediaEditor/MediaEditorFabricCropRect.js'; @@ -44,12 +39,10 @@ import { createLogger } from '../logging/log.js'; import { Button, ButtonVariant } from './Button.js'; import { CompositionInput } from './CompositionInput.js'; import { ContextMenu } from './ContextMenu.js'; -import { EmojiButton } from './emoji/EmojiButton.js'; import { IMAGE_PNG } from '../types/MIME.js'; import { SizeObserver } from '../hooks/useSizeObserver.js'; import { Slider } from './Slider.js'; import { Spinner } from './Spinner.js'; -import { StickerButton } from './stickers/StickerButton.js'; import { Theme } from '../util/theme.js'; import { ThemeType } from '../types/Util.js'; import { arrow } from '../util/keyboard.js'; @@ -60,7 +53,6 @@ import { hydrateRanges } from '../types/BodyRange.js'; import { useConfirmDiscard } from '../hooks/useConfirmDiscard.js'; import { useFabricHistory } from '../mediaEditor/useFabricHistory.js'; import { usePortal } from '../hooks/usePortal.js'; -import { isFunPickerEnabled } from './fun/isFunPickerEnabled.js'; import { FunEmojiPicker } from './fun/FunEmojiPicker.js'; import { FunEmojiPickerButton, @@ -94,20 +86,19 @@ export type PropsType = { imageToBlurHash: typeof imageToBlurHash; onClose: () => unknown; onDone: (result: MediaEditorResultType) => unknown; -} & Pick & - Pick< - CompositionInputProps, - | 'draftText' - | 'draftBodyRanges' - | 'getPreferredBadge' - | 'isFormattingEnabled' - | 'onPickEmoji' - | 'onTextTooLong' - | 'ourConversationId' - | 'platform' - | 'sortedGroupMembers' - > & - Omit; +} & Pick< + CompositionInputProps, + | 'draftText' + | 'draftBodyRanges' + | 'getPreferredBadge' + | 'isFormattingEnabled' + | 'emojiSkinToneDefault' + | 'onSelectEmoji' + | 'onTextTooLong' + | 'ourConversationId' + | 'platform' + | 'sortedGroupMembers' +>; const INITIAL_IMAGE_STATE: ImageStateType = { angle: 0, @@ -171,20 +162,12 @@ export function MediaEditor({ draftBodyRanges, getPreferredBadge, isFormattingEnabled, - onPickEmoji, + emojiSkinToneDefault, + onSelectEmoji, onTextTooLong, ourConversationId, platform, sortedGroupMembers, - - // EmojiPickerProps - onEmojiSkinToneDefaultChange, - recentEmojis, - emojiSkinToneDefault, - - // StickerButtonProps - installedPacks, - recentStickers, ...props }: PropsType): JSX.Element | null { const [fabricCanvas, setFabricCanvas] = useState(); @@ -209,23 +192,6 @@ export function MediaEditor({ const [imageState, setImageState] = useState(INITIAL_IMAGE_STATE); - const closeEmojiPickerAndFocusComposer = useCallback(() => { - if (inputApiRef.current) { - inputApiRef.current.focus(); - } - setEmojiPickerOpen(false); - }, [inputApiRef]); - - const insertEmoji = useCallback( - (e: EmojiPickDataType) => { - if (inputApiRef.current) { - inputApiRef.current.insertEmoji(e); - onPickEmoji(e); - } - }, - [inputApiRef, onPickEmoji] - ); - const handleEmojiPickerOpenChange = useCallback((open: boolean) => { setEmojiPickerOpen(open); }, []); @@ -234,16 +200,11 @@ export function MediaEditor({ setStickerPickerOpen(open); }, []); - const handleSelectEmoji = useCallback( - (emojiSelection: FunEmojiSelection) => { - const data: EmojiPickDataType = { - shortName: emojiSelection.englishShortName, - skinTone: emojiSelection.skinTone, - }; - insertEmoji(data); - }, - [insertEmoji] - ); + const handleSelectEmoji = useCallback((emojiSelection: FunEmojiSelection) => { + if (inputApiRef.current) { + inputApiRef.current.insertEmoji(emojiSelection); + } + }, []); const handlePickSticker = useCallback( (_packId: string, _stickerId: number, src: string) => { @@ -1333,48 +1294,17 @@ export function MediaEditor({ }} type="button" /> - {!isFunPickerEnabled() && ( - { - setStickerPickerOpen(value); - }} - clearInstalledStickerPack={noop} - clearShowIntroduction={() => { - // We're using this as a callback for when the sticker button - // is pressed. - fabricCanvas?.discardActiveObject(); - setEditMode(undefined); - }} - clearShowPickerHint={noop} - i18n={i18n} - installedPacks={installedPacks} - knownPacks={[]} - onPickSticker={handlePickSticker} - onPickTimeSticker={handlePickTimeSticker} - receivedPacks={[]} - recentStickers={recentStickers} - showPickerHint={false} - theme={Theme.Dark} - /> - )} - {isFunPickerEnabled() && ( - - - - )} + + +
@@ -1393,7 +1323,7 @@ export function MediaEditor({ setCaption(messageText); }} emojiSkinToneDefault={emojiSkinToneDefault ?? null} - onPickEmoji={onPickEmoji} + onSelectEmoji={onSelectEmoji} onSubmit={noop} onTextTooLong={onTextTooLong} ourConversationId={ourConversationId} @@ -1414,32 +1344,16 @@ export function MediaEditor({ // link previews not displayed with media linkPreviewResult={null} > - {!isFunPickerEnabled() && ( - setEmojiPickerOpen(true)} - onClose={closeEmojiPickerAndFocusComposer} - recentEmojis={recentEmojis} - emojiSkinToneDefault={emojiSkinToneDefault} - onEmojiSkinToneDefaultChange={ - onEmojiSkinToneDefaultChange - } - /> - )} - {isFunPickerEnabled() && ( - - - - )} + + +
) : (
diff --git a/ts/components/conversation/Quote.stories.tsx b/ts/components/conversation/Quote.stories.tsx index 23e463ca1c..98e7c5d3e2 100644 --- a/ts/components/conversation/Quote.stories.tsx +++ b/ts/components/conversation/Quote.stories.tsx @@ -117,7 +117,6 @@ const defaultMessageProps: TimelineMessagesProps = { previews: [], reactToMessage: action('default--reactToMessage'), readStatus: ReadStatus.Read, - renderEmojiPicker: () =>
, renderReactionPicker: () =>
, renderAudioAttachment: () =>
*AudioAttachment*
, setMessageToEdit: action('setMessageToEdit'), diff --git a/ts/components/conversation/ReactionPicker.stories.tsx b/ts/components/conversation/ReactionPicker.stories.tsx index 8e1a8472b7..22aa997f14 100644 --- a/ts/components/conversation/ReactionPicker.stories.tsx +++ b/ts/components/conversation/ReactionPicker.stories.tsx @@ -6,29 +6,10 @@ import { action } from '@storybook/addon-actions'; import type { Meta } from '@storybook/react'; import type { Props as ReactionPickerProps } from './ReactionPicker.js'; import { ReactionPicker } from './ReactionPicker.js'; -import { EmojiPicker } from '../emoji/EmojiPicker.js'; import { DEFAULT_PREFERRED_REACTION_EMOJI } from '../../reactions/constants.js'; -import { EmojiSkinTone } from '../fun/data/emojis.js'; const { i18n } = window.SignalContext; -const renderEmojiPicker: ReactionPickerProps['renderEmojiPicker'] = ({ - onClose, - onPickEmoji, - onEmojiSkinToneDefaultChange, - ref, -}) => ( - -); - export default { title: 'Components/Conversation/ReactionPicker', } satisfies Meta; @@ -38,12 +19,7 @@ export function Base(): JSX.Element { ); } @@ -57,14 +33,7 @@ export function SelectedReaction(): JSX.Element { i18n={i18n} selected={e} onPick={action('onPick')} - onEmojiSkinToneDefaultChange={action( - 'onEmojiSkinToneDefaultChange' - )} - openCustomizePreferredReactionsModal={action( - 'openCustomizePreferredReactionsModal' - )} preferredReactionEmoji={DEFAULT_PREFERRED_REACTION_EMOJI} - renderEmojiPicker={renderEmojiPicker} />
))} diff --git a/ts/components/conversation/ReactionPicker.tsx b/ts/components/conversation/ReactionPicker.tsx index 4edea14895..e57f0dfadd 100644 --- a/ts/components/conversation/ReactionPicker.tsx +++ b/ts/components/conversation/ReactionPicker.tsx @@ -3,39 +3,24 @@ import React, { useCallback, useState, useEffect } from 'react'; import { Button } from 'react-aria-components'; -import { convertShortName } from '../emoji/lib.js'; -import type { Props as EmojiPickerProps } from '../emoji/EmojiPicker.js'; import { useDelayedRestoreFocus } from '../../hooks/useRestoreFocus.js'; import type { LocalizerType, ThemeType } from '../../types/Util.js'; import { ReactionPickerPicker, ReactionPickerPickerEmojiButton, - ReactionPickerPickerMoreButton, ReactionPickerPickerStyle, } from '../ReactionPickerPicker.js'; -import type { EmojiSkinTone, EmojiVariantKey } from '../fun/data/emojis.js'; +import type { EmojiVariantKey } from '../fun/data/emojis.js'; import { getEmojiVariantByKey } from '../fun/data/emojis.js'; import { FunEmojiPicker } from '../fun/FunEmojiPicker.js'; import type { FunEmojiSelection } from '../fun/panels/FunPanelEmojis.js'; -import { isFunPickerEnabled } from '../fun/isFunPickerEnabled.js'; - -export type RenderEmojiPickerProps = Pick & - Pick< - EmojiPickerProps, - 'onClickSettings' | 'onPickEmoji' | 'onEmojiSkinToneDefaultChange' - > & { - ref: React.Ref; - }; export type OwnProps = { i18n: LocalizerType; selected?: string; onClose?: () => unknown; onPick: (emoji: string) => unknown; - onEmojiSkinToneDefaultChange: (emojiSkinTone: EmojiSkinTone) => unknown; - openCustomizePreferredReactionsModal?: () => unknown; preferredReactionEmoji: ReadonlyArray; - renderEmojiPicker: (props: RenderEmojiPickerProps) => React.ReactElement; theme?: ThemeType; messageEmojis?: ReadonlyArray; }; @@ -48,10 +33,7 @@ export const ReactionPicker = React.forwardRef( i18n, onClose, onPick, - onEmojiSkinToneDefaultChange, - openCustomizePreferredReactionsModal, preferredReactionEmoji, - renderEmojiPicker, selected, style, theme, @@ -80,14 +62,6 @@ export const ReactionPicker = React.forwardRef( setEmojiPickerOpen(open); }, []); - // Handle EmojiPicker::onPickEmoji - const onPickEmoji: EmojiPickerProps['onPickEmoji'] = React.useCallback( - ({ shortName, skinTone: pickedSkinTone }) => { - onPick(convertShortName(shortName, pickedSkinTone)); - }, - [onPick] - ); - const onSelectEmoji = useCallback( (emojiSelection: FunEmojiSelection) => { const variant = getEmojiVariantByKey(emojiSelection.variantKey); @@ -99,17 +73,6 @@ export const ReactionPicker = React.forwardRef( // Focus first button and restore focus on unmount const [focusRef] = useDelayedRestoreFocus(); - if (!isFunPickerEnabled() && emojiPickerOpen) { - return renderEmojiPicker({ - onClickSettings: openCustomizePreferredReactionsModal, - onClose, - onPickEmoji, - onEmojiSkinToneDefaultChange, - ref, - style, - }); - } - const otherSelected = selected != null && !preferredReactionEmoji.includes(selected); @@ -149,32 +112,20 @@ export const ReactionPicker = React.forwardRef( title={i18n('icu:Reactions--remove')} /> ) : ( - <> - {isFunPickerEnabled() && ( - - - )} - - {open ? ( -
- - {({ ref, style }) => ( - { - onPickEmoji(ev); - if (closeOnPick) { - handleClose(); - } - }} - onClose={handleClose} - emojiSkinToneDefault={emojiSkinToneDefault} - onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange} - wasInvokedFromKeyboard={wasInvokedFromKeyboard} - recentEmojis={recentEmojis} - /> - )} - -
- ) : null} - - ); -}); diff --git a/ts/components/emoji/EmojiPicker.stories.tsx b/ts/components/emoji/EmojiPicker.stories.tsx deleted file mode 100644 index 6db96644dc..0000000000 --- a/ts/components/emoji/EmojiPicker.stories.tsx +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import * as React from 'react'; -import { action } from '@storybook/addon-actions'; -import type { Meta } from '@storybook/react'; -import type { Props } from './EmojiPicker.js'; -import { EmojiPicker } from './EmojiPicker.js'; -import { EmojiSkinTone } from '../fun/data/emojis.js'; - -const { i18n } = window.SignalContext; - -export default { - title: 'Components/Emoji/EmojiPicker', -} satisfies Meta; - -export function Base(): JSX.Element { - return ( - - ); -} - -export function NoRecents(): JSX.Element { - return ( - - ); -} - -export function WithSettingsButton(): JSX.Element { - return ( - - ); -} diff --git a/ts/components/emoji/EmojiPicker.tsx b/ts/components/emoji/EmojiPicker.tsx deleted file mode 100644 index 793e2dd12f..0000000000 --- a/ts/components/emoji/EmojiPicker.tsx +++ /dev/null @@ -1,618 +0,0 @@ -// Copyright 2019 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import * as React from 'react'; -import classNames from 'classnames'; -import type { - GridCellRenderer, - SectionRenderedParams, -} from 'react-virtualized'; -import { AutoSizer, Grid } from 'react-virtualized'; -import lodash from 'lodash'; -import { FocusScope } from 'react-aria'; -import { dataByCategory } from './lib.js'; -import type { LocalizerType } from '../../types/Util.js'; -import { isSingleGrapheme } from '../../util/grapheme.js'; -import { missingCaseError } from '../../util/missingCaseError.js'; -import { FunStaticEmoji } from '../fun/FunEmoji.js'; -import { strictAssert } from '../../util/assert.js'; -import { - EMOJI_SKIN_TONE_ORDER, - emojiParentKeyConstant, - EmojiSkinTone, - emojiVariantConstant, - getEmojiParentKeyByEnglishShortName, - getEmojiVariantByParentKeyAndSkinTone, - isEmojiEnglishShortName, - EMOJI_SKIN_TONE_TO_NUMBER, - getEmojiParentByKey, -} from '../fun/data/emojis.js'; -import { useFunEmojiSearch } from '../fun/useFunEmojiSearch.js'; - -const { chunk, clamp, debounce, findLast, flatMap, initial, last, zipObject } = - lodash; - -export type EmojiPickDataType = { - skinTone: EmojiSkinTone; - shortName: string; -}; - -export type OwnProps = { - readonly i18n: LocalizerType; - readonly recentEmojis?: ReadonlyArray; - readonly emojiSkinToneDefault: EmojiSkinTone | null; - readonly onClickSettings?: () => unknown; - readonly onClose?: () => unknown; - readonly onPickEmoji: (o: EmojiPickDataType) => unknown; - readonly onEmojiSkinToneDefaultChange?: ( - emojiSkinTone: EmojiSkinTone - ) => void; - readonly wasInvokedFromKeyboard: boolean; -}; - -export type Props = OwnProps & Pick, 'style'>; - -function isEventFromMouse( - event: - | React.MouseEvent - | React.KeyboardEvent -): boolean { - return ( - ('clientX' in event && event.clientX !== 0) || - ('clientY' in event && event.clientY !== 0) - ); -} - -function focusOnRender(el: HTMLElement | null) { - if (el) { - el.focus(); - } -} - -const COL_COUNT = 8; - -const categories = [ - 'recents', - 'emoji', - 'animal', - 'food', - 'activity', - 'travel', - 'object', - 'symbol', - 'flag', -] as const; - -type Category = (typeof categories)[number]; - -export const EmojiPicker = React.memo( - React.forwardRef( - ( - { - i18n, - onPickEmoji, - emojiSkinToneDefault, - onEmojiSkinToneDefaultChange, - recentEmojis = [], - style, - onClickSettings, - onClose, - wasInvokedFromKeyboard, - }: Props, - ref - ) => { - const isRTL = i18n.getLocaleDirection() === 'rtl'; - - const [isUsingKeyboard, setIsUsingKeyboard] = React.useState( - wasInvokedFromKeyboard - ); - - const [firstRecent] = React.useState(recentEmojis); - const [selectedCategory, setSelectedCategory] = React.useState( - categories[0] - ); - const [searchMode, setSearchMode] = React.useState(false); - const [searchText, setSearchText] = React.useState(''); - const [scrollToRow, setScrollToRow] = React.useState(0); - const [selectedTone, setSelectedTone] = - React.useState(emojiSkinToneDefault); - - const emojiSearch = useFunEmojiSearch(); - - const handleToggleSearch = React.useCallback( - ( - e: - | React.MouseEvent - | React.KeyboardEvent - ) => { - if (isEventFromMouse(e)) { - setIsUsingKeyboard(false); - } - e.stopPropagation(); - e.preventDefault(); - - setSearchText(''); - setSelectedCategory(categories[0]); - setSearchMode(m => !m); - }, - [setSearchText, setSearchMode] - ); - - const debounceSearchChange = React.useMemo( - () => - debounce((query: string) => { - setScrollToRow(0); - setSearchText(query); - }, 200), - [setSearchText, setScrollToRow] - ); - - const handleSearchChange = React.useCallback( - (e: React.ChangeEvent) => { - debounceSearchChange(e.currentTarget.value); - }, - [debounceSearchChange] - ); - - const handlePickEmoji = React.useCallback( - ( - e: - | React.MouseEvent - | React.KeyboardEvent - ) => { - const { shortName } = e.currentTarget.dataset; - if ('key' in e) { - if (e.key === 'Enter') { - if (shortName && isUsingKeyboard) { - onPickEmoji({ - skinTone: selectedTone ?? EmojiSkinTone.None, - shortName, - }); - e.stopPropagation(); - e.preventDefault(); - } else if (onClose) { - onClose(); - e.stopPropagation(); - e.preventDefault(); - } - } - } else if (shortName) { - if (isEventFromMouse(e)) { - setIsUsingKeyboard(false); - } - e.stopPropagation(); - e.preventDefault(); - onPickEmoji({ - skinTone: selectedTone ?? EmojiSkinTone.None, - shortName, - }); - } - }, - [ - onClose, - onPickEmoji, - isUsingKeyboard, - selectedTone, - setIsUsingKeyboard, - ] - ); - - // Handle key presses, particularly Escape - React.useEffect(() => { - const handler = (event: KeyboardEvent) => { - if (event.key === 'Tab') { - // We do NOT prevent default here to allow Tab to be used normally - setIsUsingKeyboard(true); - return; - } - if (event.key === 'Escape') { - if (searchMode) { - event.preventDefault(); - event.stopPropagation(); - setScrollToRow(0); - setSearchText(''); - setSearchMode(false); - } else if (onClose) { - event.preventDefault(); - event.stopPropagation(); - onClose(); - } - } else if (!searchMode && !event.ctrlKey && !event.metaKey) { - if ( - [ - 'ArrowUp', - 'ArrowDown', - 'ArrowLeft', - 'ArrowRight', - 'Enter', - 'Shift', - ' ', // Space - ].includes(event.key) - ) { - // Do nothing, these can be used to navigate around the picker. - } else if (isSingleGrapheme(event.key)) { - // A single grapheme means the user is typing text. Switch to search mode. - setSelectedCategory(categories[0]); - setSearchMode(true); - // Continue propagation, typing the first letter for search. - } else { - // For anything else, assume it's a special key that isn't one of the ones - // above (such as Delete or ContextMenu). - onClose?.(); - event.preventDefault(); - event.stopPropagation(); - } - } - }; - - document.addEventListener('keydown', handler); - - return () => { - document.removeEventListener('keydown', handler); - }; - }, [onClose, setIsUsingKeyboard, searchMode, setSearchMode]); - - const [, ...renderableCategories] = categories; - - const emojiGrid = React.useMemo(() => { - if (searchText) { - return chunk( - emojiSearch(searchText).map(result => { - const parent = getEmojiParentByKey(result.parentKey); - return parent.englishShortNameDefault; - }), - COL_COUNT - ); - } - - const chunks = flatMap(renderableCategories, cat => - chunk( - dataByCategory[cat].map(e => e.short_name), - COL_COUNT - ) - ); - - return [...chunk(firstRecent, COL_COUNT), ...chunks]; - }, [firstRecent, renderableCategories, searchText, emojiSearch]); - - const rowCount = emojiGrid.length; - - const catRowEnds = React.useMemo(() => { - const rowEnds: Array = [ - Math.ceil(firstRecent.length / COL_COUNT) - 1, - ]; - - renderableCategories.forEach(cat => { - rowEnds.push( - Math.ceil(dataByCategory[cat].length / COL_COUNT) + - (last(rowEnds) as number) - ); - }); - - return rowEnds; - }, [firstRecent.length, renderableCategories]); - - const catToRowOffsets = React.useMemo(() => { - const offsets = initial(catRowEnds).map(i => i + 1); - - return zipObject(categories, [0, ...offsets]); - }, [catRowEnds]); - - const catOffsetEntries = React.useMemo( - () => Object.entries(catToRowOffsets), - [catToRowOffsets] - ); - - const handleSelectCategory = React.useCallback( - ( - e: - | React.MouseEvent - | React.KeyboardEvent - ) => { - e.stopPropagation(); - e.preventDefault(); - - const { category } = e.currentTarget.dataset; - if (category) { - setSelectedCategory(category as Category); - setScrollToRow(catToRowOffsets[category]); - } - }, - [catToRowOffsets, setSelectedCategory, setScrollToRow] - ); - - const cellRenderer = React.useCallback( - ({ key, style: cellStyle, rowIndex, columnIndex }) => { - const shortName = emojiGrid[rowIndex][columnIndex]; - - if (!shortName) { - return null; - } - - strictAssert( - isEmojiEnglishShortName(shortName), - 'Must be a valid emoji short name' - ); - const parentKey = getEmojiParentKeyByEnglishShortName(shortName); - const variantKey = getEmojiVariantByParentKeyAndSkinTone( - parentKey, - selectedTone ?? EmojiSkinTone.None - ); - - return ( -
- -
- ); - }, - [emojiGrid, handlePickEmoji, selectedTone] - ); - - const getRowHeight = React.useCallback( - ({ index }: { index: number }) => { - if (searchText) { - return 34; - } - - if (catRowEnds.includes(index) && index !== last(catRowEnds)) { - return 44; - } - - return 34; - }, - [catRowEnds, searchText] - ); - - const onSectionRendered = React.useMemo( - () => - debounce(({ rowStartIndex }: SectionRenderedParams) => { - const [cat] = - findLast(catOffsetEntries, ([, row]) => rowStartIndex >= row) || - categories; - - setSelectedCategory(cat as Category); - }, 10), - [catOffsetEntries] - ); - - function getCategoryButtonLabel(category: Category): string { - switch (category) { - case 'recents': - return i18n('icu:EmojiPicker__button--recents'); - case 'emoji': - return i18n('icu:EmojiPicker__button--emoji'); - case 'animal': - return i18n('icu:EmojiPicker__button--animal'); - case 'food': - return i18n('icu:EmojiPicker__button--food'); - case 'activity': - return i18n('icu:EmojiPicker__button--activity'); - case 'travel': - return i18n('icu:EmojiPicker__button--travel'); - case 'object': - return i18n('icu:EmojiPicker__button--object'); - case 'symbol': - return i18n('icu:EmojiPicker__button--symbol'); - case 'flag': - return i18n('icu:EmojiPicker__button--flag'); - default: - throw missingCaseError(category); - } - } - - return ( - -
-
-
- {rowCount > 0 ? ( -
- - {({ width, height }) => ( - - )} - -
- ) : ( -
- {i18n('icu:EmojiPicker--empty')} - - - -
- )} -
- {Boolean(onClickSettings) && ( - - ); - })} -
- ) : null} - {Boolean(onClickSettings) && ( -
- )} - -
-
- ); - } - ) -); diff --git a/ts/components/emoji/lib.ts b/ts/components/emoji/lib.ts deleted file mode 100644 index 321fe6c714..0000000000 --- a/ts/components/emoji/lib.ts +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright 2019 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -// Camelcase disabled due to emoji-datasource using snake_case -/* eslint-disable camelcase */ -import lodash from 'lodash'; -import { getOwn } from '../../util/getOwn.js'; -import { - EMOJI_SKIN_TONE_TO_KEY, - EmojiSkinTone, - KEY_TO_EMOJI_SKIN_TONE, -} from '../fun/data/emojis.js'; -import { strictAssert } from '../../util/assert.js'; - -const { groupBy, keyBy, mapValues, sortBy } = lodash; - -// Import emoji-datasource dynamically to avoid costly typechecking. -// eslint-disable-next-line import/no-dynamic-require, @typescript-eslint/no-var-requires -const untypedData = require('emoji-datasource' as string); - -export const skinTones = ['1F3FB', '1F3FC', '1F3FD', '1F3FE', '1F3FF']; - -export type SkinToneKey = '1F3FB' | '1F3FC' | '1F3FD' | '1F3FE' | '1F3FF'; - -type EmojiSkinVariation = { - unified: string; - non_qualified: null; - image: string; - sheet_x: number; - sheet_y: number; - added_in: string; - has_img_apple: boolean; - has_img_google: boolean; - has_img_twitter: boolean; - has_img_emojione: boolean; - has_img_facebook: boolean; - has_img_messenger: boolean; -}; - -export type EmojiData = { - name: string; - unified: string; - non_qualified: string | null; - docomo: string | null; - au: string | null; - softbank: string | null; - google: string | null; - image: string; - sheet_x: number; - sheet_y: number; - short_name: string; - short_names: Array; - text: string | null; - texts: Array | null; - category: string; - sort_order: number; - added_in: string; - has_img_apple: boolean; - has_img_google: boolean; - has_img_twitter: boolean; - has_img_facebook: boolean; - skin_variations?: { - [key: string]: EmojiSkinVariation; - }; -}; - -export const data = (untypedData as Array) - .filter(emoji => emoji.has_img_apple) - .map(emoji => - // Why this weird map? - // the emoji dataset has two separate categories for Emotions and People - // yet in our UI we display these as a single merged category. In order - // for the emojis to be sorted properly we're manually incrementing the - // sort_order for the People & Body emojis so that they fall below the - // Smiley & Emotions category. - emoji.category === 'People & Body' - ? { ...emoji, sort_order: emoji.sort_order + 1000 } - : emoji - ); - -const dataByShortName = keyBy(data, 'short_name'); -const dataByEmoji: { [key: string]: EmojiData } = {}; - -export const dataByCategory = mapValues( - groupBy(data, ({ category }) => { - if (category === 'Activities') { - return 'activity'; - } - - if (category === 'Animals & Nature') { - return 'animal'; - } - - if (category === 'Flags') { - return 'flag'; - } - - if (category === 'Food & Drink') { - return 'food'; - } - - if (category === 'Objects') { - return 'object'; - } - - if (category === 'Travel & Places') { - return 'travel'; - } - - if (category === 'Smileys & Emotion') { - return 'emoji'; - } - - if (category === 'People & Body') { - return 'emoji'; - } - - if (category === 'Symbols') { - return 'symbol'; - } - - return 'misc'; - }), - arr => sortBy(arr, 'sort_order') -); - -export function getEmojiData( - shortName: keyof typeof dataByShortName, - emojiSkinToneDefault: EmojiSkinTone -): EmojiData | EmojiSkinVariation { - const base = dataByShortName[shortName]; - const variation = EMOJI_SKIN_TONE_TO_KEY.get(emojiSkinToneDefault); - - if (variation != null && base.skin_variations) { - if (base.skin_variations[variation]) { - return base.skin_variations[variation]; - } - - // For emojis that have two people in them which can have diff skin tones - // the Map is of SkinTone-SkinTone. If we don't find the correct skin tone - // in the list of variations then we assume it is one of those double skin - // emojis and we default to both people having same skin. - return base.skin_variations[`${variation}-${variation}`]; - } - - return base; -} - -export function unifiedToEmoji(unified: string): string { - return unified - .split('-') - .map(c => String.fromCodePoint(parseInt(c, 16))) - .join(''); -} - -export function convertShortNameToData( - shortName: string, - skinTone: EmojiSkinTone -): EmojiData | undefined { - const base = dataByShortName[shortName]; - - if (!base) { - return undefined; - } - - if (skinTone !== EmojiSkinTone.None && base.skin_variations != null) { - const toneKey = EMOJI_SKIN_TONE_TO_KEY.get(skinTone); - strictAssert(toneKey, `Missing key for skin tone: ${skinTone}`); - const variation = - base.skin_variations[toneKey] ?? - base.skin_variations[`${toneKey}-${toneKey}`]; - if (variation) { - return { - ...base, - ...variation, - }; - } - } - - return base; -} - -export function convertShortName( - shortName: string, - skinTone: EmojiSkinTone -): string { - const emojiData = convertShortNameToData(shortName, skinTone); - - if (!emojiData) { - return ''; - } - - return unifiedToEmoji(emojiData.unified); -} - -export function emojiToData(emoji: string): EmojiData | undefined { - return getOwn(dataByEmoji, emoji); -} - -data.forEach(emoji => { - const { short_name, short_names, skin_variations } = emoji; - - if (short_names) { - short_names.forEach(name => { - dataByShortName[name] = emoji; - }); - } - - dataByEmoji[convertShortName(short_name, EmojiSkinTone.None)] = emoji; - - if (skin_variations) { - Object.entries(skin_variations).forEach(([tone]) => { - const emojiSkinTone = KEY_TO_EMOJI_SKIN_TONE.get(tone); - if (emojiSkinTone != null) { - dataByEmoji[convertShortName(short_name, emojiSkinTone)] = emoji; - } - }); - } -}); diff --git a/ts/components/fun/FunEmoji.stories.tsx b/ts/components/fun/FunEmoji.stories.tsx index d97b6b4098..78ce0c90fc 100644 --- a/ts/components/fun/FunEmoji.stories.tsx +++ b/ts/components/fun/FunEmoji.stories.tsx @@ -8,7 +8,7 @@ import type { FunStaticEmojiProps } from './FunEmoji.js'; import { FunInlineEmoji, FunStaticEmoji } from './FunEmoji.js'; import { _getAllEmojiVariantKeys, - emojiVariantConstant, + EMOJI_VARIANT_KEY_CONSTANTS, getEmojiParentByKey, getEmojiParentKeyByVariantKey, getEmojiVariantByKey, @@ -121,6 +121,10 @@ export function All(props: AllProps): JSX.Element { ); } +const FRIED_SHRIMP = getEmojiVariantByKey( + EMOJI_VARIANT_KEY_CONSTANTS.FRIED_SHRIMP +); + export function Inline(): JSX.Element { return (
@@ -128,7 +132,7 @@ export function Inline(): JSX.Element { {' '} Lorem, ipsum dolor sit amet consectetur adipisicing elit. Repellat voluptates, mollitia tempora alias libero repudiandae nesciunt. Deleniti @@ -137,7 +141,7 @@ export function Inline(): JSX.Element { {' '} Consectetur quibusdam accusantium magni ipsum nemo eligendi quisquam dolor, recusandae vero dolore reiciendis doloribus ducimus officiis @@ -146,7 +150,7 @@ export function Inline(): JSX.Element {

diff --git a/ts/components/fun/data/emojis.ts b/ts/components/fun/data/emojis.ts index a4ddaa5a67..1e62e7ec2c 100644 --- a/ts/components/fun/data/emojis.ts +++ b/ts/components/fun/data/emojis.ts @@ -621,7 +621,7 @@ export function _getAllEmojiVariantKeys(): Iterable { return EMOJI_INDEX.variantByKey.keys(); } -export function emojiParentKeyConstant(input: string): EmojiParentKey { +function emojiParentKeyConstant(input: string): EmojiParentKey { strictAssert( isEmojiParentValue(input), `Missing emoji parent for value "${input}"` @@ -629,15 +629,44 @@ export function emojiParentKeyConstant(input: string): EmojiParentKey { return getEmojiParentKeyByValue(input); } -export function emojiVariantConstant(input: string): EmojiVariantData { +function emojiVariantKeyConstant(input: string): EmojiVariantKey { strictAssert( isEmojiVariantValue(input), `Missing emoji variant for value "${input}"` ); - const key = getEmojiVariantKeyByValue(input); - return getEmojiVariantByKey(key); + return getEmojiVariantKeyByValue(input); } +export const EMOJI_PARENT_KEY_CONSTANTS = { + RED_HEART: emojiParentKeyConstant('\u{2764}\u{FE0F}'), + CRYING_FACE: emojiParentKeyConstant('\u{1F622}'), + FACE_WITH_TEARS_OF_JOY: emojiParentKeyConstant('\u{1F602}'), + FACE_WITH_OPEN_MOUTH: emojiParentKeyConstant('\u{1F62E}'), + ENRAGED_FACE: emojiParentKeyConstant('\u{1F621}'), + SLIGHTLY_SMILING_FACE: emojiParentKeyConstant('\u{1F642}'), + SLIGHTLY_FROWNING_FACE: emojiParentKeyConstant('\u{1F641}'), + GRINNING_FACE: emojiParentKeyConstant('\u{1F600}'), + FACE_BLOWING_A_KISS: emojiParentKeyConstant('\u{1F618}'), + FACE_WITH_STUCK_OUT_TONGUE: emojiParentKeyConstant('\u{1F61B}'), + CONFUSED_FACE: emojiParentKeyConstant('\u{1F615}'), + NEUTRAL_FACE: emojiParentKeyConstant('\u{1F610}'), + WINKING_FACE: emojiParentKeyConstant('\u{1F609}'), + ZIPPER_MOUTH_FACE: emojiParentKeyConstant('\u{1F910}'), + THUMBS_UP: emojiParentKeyConstant('\u{1F44D}'), + THUMBS_DOWN: emojiParentKeyConstant('\u{1F44E}'), + RAISED_HAND: emojiParentKeyConstant('\u{270B}'), + WAVING_HAND: emojiParentKeyConstant('\u{1F44B}'), + HOT_BEVERAGE: emojiParentKeyConstant('\u{2615}'), + MOBILE_PHONE_OFF: emojiParentKeyConstant('\u{1F4F4}'), +} as const; + +export const EMOJI_VARIANT_KEY_CONSTANTS = { + SLIGHTLY_FROWNING_FACE: emojiVariantKeyConstant('\u{1F641}'), + GRINNING_FACE_WITH_SMILING_EYES: emojiVariantKeyConstant('\u{1F604}'), + GRINNING_CAT_WITH_SMILING_EYES: emojiVariantKeyConstant('\u{1F638}'), + FRIED_SHRIMP: emojiVariantKeyConstant('\u{1F364}'), +} as const; + /** * Completions */ diff --git a/ts/components/fun/isFunPickerEnabled.tsx b/ts/components/fun/isFunPickerEnabled.tsx deleted file mode 100644 index 0c7633184f..0000000000 --- a/ts/components/fun/isFunPickerEnabled.tsx +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2025 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -import * as RemoteConfig from '../../RemoteConfig.js'; - -export function isFunPickerEnabled(): boolean { - return RemoteConfig.isEnabled('desktop.funPicker'); -} diff --git a/ts/components/fun/panels/FunPanelEmojis.tsx b/ts/components/fun/panels/FunPanelEmojis.tsx index f0e9423b97..e0f4ad5862 100644 --- a/ts/components/fun/panels/FunPanelEmojis.tsx +++ b/ts/components/fun/panels/FunPanelEmojis.tsx @@ -48,12 +48,10 @@ import { FunSubNavListBox, FunSubNavListBoxItem, } from '../base/FunSubNav.js'; -import type { EmojiParentKey, EmojiVariantKey } from '../data/emojis.js'; +import type { EmojiVariantKey } from '../data/emojis.js'; import { EmojiSkinTone, - emojiParentKeyConstant, EmojiPickerCategory, - emojiVariantConstant, getEmojiParentByKey, getEmojiPickerCategoryParentKeys, getEmojiVariantByParentKeyAndSkinTone, @@ -61,7 +59,8 @@ import { isEmojiVariantKey, getEmojiParentKeyByVariantKey, getEmojiVariantByKey, - getEmojiSkinToneByVariantKey, + EMOJI_PARENT_KEY_CONSTANTS, + EMOJI_VARIANT_KEY_CONSTANTS, } from '../data/emojis.js'; import { useFunEmojiSearch } from '../useFunEmojiSearch.js'; import { FunKeyboard } from '../keyboard/FunKeyboard.js'; @@ -163,9 +162,6 @@ function getSelectedSection( export type FunEmojiSelection = Readonly<{ variantKey: EmojiVariantKey; - parentKey: EmojiParentKey; - englishShortName: string; - skinTone: EmojiSkinTone; }>; export type FunPanelEmojisProps = Readonly<{ @@ -479,7 +475,9 @@ export function FunPanelEmojis({ @@ -656,10 +654,6 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element { return getEmojiVariantByKey(props.value); }, [props.value]); - const skinTone = useMemo(() => { - return getEmojiSkinToneByVariantKey(emojiVariant.key); - }, [emojiVariant.key]); - const handleClick = useCallback( (event: PointerEvent) => { if (emojiHasSkinToneVariants && emojiSkinToneDefault == null) { @@ -668,9 +662,6 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element { } const emojiSelection: FunEmojiSelection = { variantKey: emojiVariant.key, - parentKey: emojiParent.key, - englishShortName: emojiParent.englishShortNameDefault, - skinTone, }; const shouldClose = event.nativeEvent.pointerType !== 'mouse' && @@ -681,9 +672,6 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element { emojiHasSkinToneVariants, emojiSkinToneDefault, emojiVariant.key, - emojiParent.key, - emojiParent.englishShortNameDefault, - skinTone, onSelectEmoji, ] ); @@ -714,19 +702,11 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element { onEmojiSkinToneDefaultChange(skinToneSelection); const emojiSelection: FunEmojiSelection = { variantKey: variant.key, - parentKey: emojiParent.key, - englishShortName: emojiParent.englishShortNameDefault, - skinTone: skinToneSelection, }; const shouldClose = true; onSelectEmoji(emojiSelection, shouldClose); }, - [ - onEmojiSkinToneDefaultChange, - emojiParent.key, - emojiParent.englishShortNameDefault, - onSelectEmoji, - ] + [onEmojiSkinToneDefaultChange, emojiParent.key, onSelectEmoji] ); const emojiName = useMemo(() => { @@ -836,7 +816,7 @@ function SectionSkinToneHeaderPopover( diff --git a/ts/components/fun/panels/FunPanelGifs.tsx b/ts/components/fun/panels/FunPanelGifs.tsx index 3e93e86935..afce0ef909 100644 --- a/ts/components/fun/panels/FunPanelGifs.tsx +++ b/ts/components/fun/panels/FunPanelGifs.tsx @@ -52,7 +52,6 @@ import { FunResultsSpinner, } from '../base/FunResults.js'; import { FunStaticEmoji } from '../FunEmoji.js'; -import { emojiVariantConstant } from '../data/emojis.js'; import { FunLightboxPortal, FunLightboxBackdrop, @@ -66,6 +65,10 @@ import type { LocalizerType } from '../../../types/I18N.js'; import { isAbortError } from '../../../util/isAbortError.js'; import { createLogger } from '../../../logging/log.js'; import * as Errors from '../../../types/errors.js'; +import { + EMOJI_VARIANT_KEY_CONSTANTS, + getEmojiVariantByKey, +} from '../data/emojis.js'; const log = createLogger('FunPanelGifs'); @@ -508,7 +511,9 @@ export function FunPanelGifs({ )} diff --git a/ts/components/fun/panels/FunPanelStickers.tsx b/ts/components/fun/panels/FunPanelStickers.tsx index 1fcaf13b40..d43401c071 100644 --- a/ts/components/fun/panels/FunPanelStickers.tsx +++ b/ts/components/fun/panels/FunPanelStickers.tsx @@ -9,6 +9,7 @@ import React, { useRef, useState, } from 'react'; +import { VisuallyHidden } from 'react-aria'; import type { StickerPackType, StickerType, @@ -55,9 +56,10 @@ import { FunSubNavScroller, } from '../base/FunSubNav.js'; import { + EMOJI_VARIANT_KEY_CONSTANTS, type EmojiParentKey, - emojiVariantConstant, getEmojiParentKeyByValue, + getEmojiVariantByKey, isEmojiParentValue, } from '../data/emojis.js'; import { FunKeyboard } from '../keyboard/FunKeyboard.js'; @@ -455,6 +457,9 @@ export function FunPanelStickers({ {onAddStickerPack != null && ( + + {i18n('icu:FunPanelStickers__SubNavButton--AddStickerPack')} + @@ -475,7 +480,9 @@ export function FunPanelStickers({ diff --git a/ts/components/stickers/StickerButton.stories.tsx b/ts/components/stickers/StickerButton.stories.tsx deleted file mode 100644 index f3e1d145a1..0000000000 --- a/ts/components/stickers/StickerButton.stories.tsx +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import * as React from 'react'; -import { action } from '@storybook/addon-actions'; -import type { Meta } from '@storybook/react'; -import type { Props } from './StickerButton.js'; -import { StickerButton } from './StickerButton.js'; -import { - createPack, - sticker1, - sticker2, - tallSticker, - wideSticker, -} from './mocks.js'; - -const { i18n } = window.SignalContext; - -export default { - title: 'Components/Stickers/StickerButton', - decorators: [ - storyFn => ( -
- {storyFn()} -
- ), - ], - argTypes: { - showIntroduction: { control: { type: 'boolean' } }, - showPickerHint: { control: { type: 'boolean' } }, - }, - args: { - blessedPacks: [], - clearInstalledStickerPack: action('clearInstalledStickerPack'), - clearShowIntroduction: action('clearShowIntroduction'), - clearShowPickerHint: action('clearShowPickerHint'), - i18n, - installedPacks: [], - knownPacks: [], - onClickAddPack: action('onClickAddPack'), - onPickSticker: action('onPickSticker'), - receivedPacks: [], - recentStickers: [], - showIntroduction: false, - showPickerHint: false, - }, -} satisfies Meta; - -const receivedPacks = [ - createPack({ id: 'received-pack-1', status: 'downloaded' }, sticker1), - createPack({ id: 'received-pack-2', status: 'downloaded' }, sticker2), -]; - -const installedPacks = [ - createPack({ id: 'installed-pack-1', status: 'installed' }, sticker1), - createPack({ id: 'installed-pack-2', status: 'installed' }, sticker2), -]; - -const blessedPacks = [ - createPack( - { id: 'blessed-pack-1', status: 'downloaded', isBlessed: true }, - sticker1 - ), - createPack( - { id: 'blessed-pack-2', status: 'downloaded', isBlessed: true }, - sticker2 - ), -]; - -const knownPacks = [ - createPack({ id: 'known-pack-1', status: 'known' }, sticker1), - createPack({ id: 'known-pack-2', status: 'known' }, sticker2), -]; - -export function OnlyInstalled(args: Props): JSX.Element { - return ; -} - -export function OnlyReceived(args: Props): JSX.Element { - return ; -} - -export function OnlyKnown(args: Props): JSX.Element { - return ; -} - -export function OnlyBlessed(args: Props): JSX.Element { - return ; -} - -export function NoPacks(args: Props): JSX.Element { - return ; -} - -export function InstalledPackTooltip(args: Props): JSX.Element { - return ( - - ); -} - -export function InstalledPackTooltipWide(args: Props): JSX.Element { - const installedPack = createPack({ id: 'installed-pack-wide' }, wideSticker); - - return ( - - ); -} - -export function InstalledPackTooltipTall(args: Props): JSX.Element { - const installedPack = createPack({ id: 'installed-pack-tall' }, tallSticker); - return ( - - ); -} - -export function NewInstallTooltip(args: Props): JSX.Element { - return ( - - ); -} diff --git a/ts/components/stickers/StickerButton.tsx b/ts/components/stickers/StickerButton.tsx deleted file mode 100644 index 8207744ec6..0000000000 --- a/ts/components/stickers/StickerButton.tsx +++ /dev/null @@ -1,390 +0,0 @@ -// Copyright 2019 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import * as React from 'react'; -import classNames from 'classnames'; -import lodash from 'lodash'; -import { Manager, Popper, Reference } from 'react-popper'; -import { createPortal } from 'react-dom'; - -import type { - StickerPackType, - StickerType, -} from '../../state/ducks/stickers.js'; -import type { LocalizerType } from '../../types/Util.js'; -import type { Theme } from '../../util/theme.js'; -import { StickerPicker } from './StickerPicker.js'; -import { countStickers } from './lib.js'; -import { offsetDistanceModifier } from '../../util/popperUtil.js'; -import { themeClassName } from '../../util/theme.js'; -import { handleOutsideClick } from '../../util/handleOutsideClick.js'; -import * as KeyboardLayout from '../../services/keyboardLayout.js'; -import { useRefMerger } from '../../hooks/useRefMerger.js'; -import { UserText } from '../UserText.js'; - -const { get, noop } = lodash; - -export type OwnProps = { - readonly className?: string; - readonly i18n: LocalizerType; - readonly receivedPacks: ReadonlyArray; - readonly installedPacks: ReadonlyArray; - readonly blessedPacks: ReadonlyArray; - readonly knownPacks: ReadonlyArray; - readonly installedPack?: StickerPackType | null; - readonly recentStickers: ReadonlyArray; - readonly onOpenStateChanged?: (isOpen: boolean) => void; - readonly clearInstalledStickerPack: () => unknown; - readonly onClickAddPack?: () => unknown; - readonly onPickSticker: ( - packId: string, - stickerId: number, - url: string - ) => unknown; - readonly onPickTimeSticker?: (style: 'analog' | 'digital') => unknown; - readonly showIntroduction?: boolean; - readonly clearShowIntroduction: () => unknown; - readonly showPickerHint: boolean; - readonly clearShowPickerHint: () => unknown; - readonly position?: 'top-end' | 'top-start'; - readonly theme?: Theme; -}; - -export type Props = OwnProps; - -export const StickerButton = React.memo(function StickerButtonInner({ - className, - i18n, - clearInstalledStickerPack, - onClickAddPack, - onPickSticker, - onPickTimeSticker, - recentStickers, - onOpenStateChanged, - receivedPacks, - installedPack, - installedPacks, - blessedPacks, - knownPacks, - showIntroduction, - clearShowIntroduction, - showPickerHint, - clearShowPickerHint, - position = 'top-end', - theme, -}: Props) { - const [open, internalSetOpen] = React.useState(false); - - const setOpen = React.useCallback( - (value: boolean) => { - internalSetOpen(value); - if (onOpenStateChanged) { - onOpenStateChanged(value); - } - }, - [internalSetOpen, onOpenStateChanged] - ); - - const [popperRoot, setPopperRoot] = React.useState(null); - const buttonRef = React.useRef(null); - const refMerger = useRefMerger(); - - const handleClickButton = React.useCallback(() => { - // Clear tooltip state - clearInstalledStickerPack(); - clearShowIntroduction(); - - // Handle button click - if (installedPacks.length === 0) { - onClickAddPack?.(); - } else if (popperRoot) { - setOpen(false); - } else { - setOpen(true); - } - }, [ - clearInstalledStickerPack, - clearShowIntroduction, - installedPacks, - onClickAddPack, - popperRoot, - setOpen, - ]); - - const handlePickSticker = React.useCallback( - (packId: string, stickerId: number, url: string) => { - setOpen(false); - onPickSticker(packId, stickerId, url); - }, - [setOpen, onPickSticker] - ); - - const handlePickTimeSticker = React.useCallback( - (style: 'analog' | 'digital') => { - setOpen(false); - onPickTimeSticker?.(style); - }, - [setOpen, onPickTimeSticker] - ); - - const handleClose = React.useCallback(() => { - setOpen(false); - }, [setOpen]); - - const handleClickAddPack = React.useCallback(() => { - setOpen(false); - if (showPickerHint) { - clearShowPickerHint(); - } - onClickAddPack?.(); - }, [onClickAddPack, showPickerHint, setOpen, clearShowPickerHint]); - - const handleClearIntroduction = React.useCallback(() => { - clearInstalledStickerPack(); - clearShowIntroduction(); - }, [clearInstalledStickerPack, clearShowIntroduction]); - - // Create popper root and handle outside clicks - React.useEffect(() => { - if (open) { - const root = document.createElement('div'); - setPopperRoot(root); - document.body.appendChild(root); - - return () => { - document.body.removeChild(root); - setPopperRoot(null); - }; - } - - return noop; - }, [open, setOpen, setPopperRoot]); - - React.useEffect(() => { - if (!open) { - return noop; - } - - return handleOutsideClick( - target => { - const targetElement = target as HTMLElement; - const targetClassName = targetElement - ? targetElement.className || '' - : ''; - - // We need to special-case sticker picker header buttons, because they can - // disappear after being clicked, which breaks the .contains() check below. - const isMissingButtonClass = - !targetClassName || - targetClassName.indexOf('module-sticker-picker__header__button') < 0; - - if (!isMissingButtonClass) { - return false; - } - - setOpen(false); - return true; - }, - { - containerElements: [popperRoot, buttonRef], - name: 'StickerButton', - } - ); - }, [open, popperRoot, setOpen]); - - // Install keyboard shortcut to open sticker picker - React.useEffect(() => { - const handleKeydown = (event: KeyboardEvent) => { - const { ctrlKey, metaKey, shiftKey } = event; - const commandKey = get(window, 'platform') === 'darwin' && metaKey; - const controlKey = get(window, 'platform') !== 'darwin' && ctrlKey; - const commandOrCtrl = commandKey || controlKey; - const key = KeyboardLayout.lookup(event); - - // We don't want to open up if the conversation has any panels open - const panels = document.querySelectorAll('.conversation .panel'); - if (panels && panels.length > 1) { - return; - } - - if (commandOrCtrl && shiftKey && (key === 'g' || key === 'G')) { - event.stopPropagation(); - event.preventDefault(); - - setOpen(!open); - } - }; - document.addEventListener('keydown', handleKeydown); - - return () => { - document.removeEventListener('keydown', handleKeydown); - }; - }, [open, setOpen]); - - // Clear the installed pack after one minute - React.useEffect(() => { - if (installedPack) { - const timerId = setTimeout(clearInstalledStickerPack, 10 * 1000); - - return () => { - clearTimeout(timerId); - }; - } - - return noop; - }, [installedPack, clearInstalledStickerPack]); - - if ( - countStickers({ - knownPacks, - blessedPacks, - installedPacks, - receivedPacks, - }) === 0 - ) { - return null; - } - - return ( - - - {({ ref }) => ( - -
- )} - - ) : null} - {!open && showIntroduction ? ( - - {({ ref, style, placement, arrowProps }) => ( -
-
-
- -
- )} -
- ) : null} - {open && popperRoot - ? createPortal( - - {({ ref, style }) => ( -
- -
- )} -
, - popperRoot - ) - : null} - - ); -}); diff --git a/ts/components/stickers/StickerPicker.stories.tsx b/ts/components/stickers/StickerPicker.stories.tsx deleted file mode 100644 index 57863a96c6..0000000000 --- a/ts/components/stickers/StickerPicker.stories.tsx +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import * as React from 'react'; -import { action } from '@storybook/addon-actions'; -import type { Meta } from '@storybook/react'; -import type { Props } from './StickerPicker.js'; -import { StickerPicker } from './StickerPicker.js'; -import { abeSticker, createPack, packs, recentStickers } from './mocks.js'; - -const { i18n } = window.SignalContext; - -export default { - title: 'Components/Stickers/StickerPicker', - component: StickerPicker, - argTypes: { - showPickerHint: { control: { type: 'boolean' } }, - }, - args: { - i18n, - onClickAddPack: action('onClickAddPack'), - onClose: action('onClose'), - onPickSticker: action('onPickSticker'), - packs: [], - recentStickers: [], - showPickerHint: false, - }, -} satisfies Meta; - -export function Full(args: Props): JSX.Element { - return ( - - ); -} - -export function PickerHint(args: Props): JSX.Element { - return ( - - ); -} - -export function NoRecentStickers(args: Props): JSX.Element { - return ; -} - -export function Empty(args: Props): JSX.Element { - return ; -} - -export function PendingDownload(args: Props): JSX.Element { - const pack = createPack( - { status: 'pending', stickers: [abeSticker] }, - abeSticker - ); - - return ; -} - -export function Error(args: Props): JSX.Element { - const pack = createPack( - { status: 'error', stickers: [abeSticker] }, - abeSticker - ); - - return ; -} - -export function NoCover(args: Props): JSX.Element { - const pack = createPack({ status: 'error', stickers: [abeSticker] }); - return ; -} diff --git a/ts/components/stickers/StickerPicker.tsx b/ts/components/stickers/StickerPicker.tsx deleted file mode 100644 index 153cde7072..0000000000 --- a/ts/components/stickers/StickerPicker.tsx +++ /dev/null @@ -1,408 +0,0 @@ -// Copyright 2019 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import classNames from 'classnames'; -import * as React from 'react'; -import { FocusScope } from 'react-aria'; -import { useRestoreFocus } from '../../hooks/useRestoreFocus.js'; -import type { - StickerPackType, - StickerType, -} from '../../state/ducks/stickers.js'; -import type { LocalizerType } from '../../types/Util.js'; -import { getDateTimeFormatter } from '../../util/formatTimestamp.js'; -import { getAnalogTime } from '../../util/getAnalogTime.js'; - -export type OwnProps = { - readonly i18n: LocalizerType; - readonly onClose: () => unknown; - readonly onClickAddPack?: () => unknown; - readonly onPickSticker: ( - packId: string, - stickerId: number, - url: string - ) => unknown; - readonly onPickTimeSticker?: (style: 'analog' | 'digital') => unknown; - readonly packs: ReadonlyArray; - readonly recentStickers: ReadonlyArray; - readonly showPickerHint?: boolean; -}; - -export type Props = OwnProps & Pick, 'style'>; - -function useTabs(tabs: ReadonlyArray, initialTab = tabs[0]) { - const [tab, setTab] = React.useState(initialTab); - const handlers = React.useMemo( - () => - tabs.map(t => () => { - setTab(t); - }), - [tabs] - ); - - return [tab, handlers] as [T, ReadonlyArray<() => void>]; -} - -const PACKS_PAGE_SIZE = 7; -const PACK_ICON_WIDTH = 32; -const PACK_PAGE_WIDTH = PACKS_PAGE_SIZE * PACK_ICON_WIDTH; - -function getPacksPageOffset(page: number, packs: number): number { - if (page === 0) { - return 0; - } - - if (isLastPacksPage(page, packs)) { - return ( - PACK_PAGE_WIDTH * (Math.floor(packs / PACKS_PAGE_SIZE) - 1) + - ((packs % PACKS_PAGE_SIZE) - 1) * PACK_ICON_WIDTH - ); - } - - return page * PACK_ICON_WIDTH * PACKS_PAGE_SIZE; -} - -function isLastPacksPage(page: number, packs: number): boolean { - return page === Math.floor(packs / PACKS_PAGE_SIZE); -} - -export const StickerPicker = React.memo( - React.forwardRef( - ( - { - i18n, - packs, - recentStickers, - onClose, - onClickAddPack, - onPickSticker, - onPickTimeSticker, - showPickerHint, - style, - }: Props, - ref - ) => { - const tabIds = React.useMemo( - () => ['recents', ...packs.map(({ id }) => id)], - [packs] - ); - const [currentTab, [recentsHandler, ...packsHandlers]] = useTabs( - tabIds, - // If there are no recent stickers, - // default to the first sticker pack, - // unless there are no sticker packs. - tabIds[recentStickers.length > 0 ? 0 : Math.min(1, tabIds.length)] - ); - const selectedPack = packs.find(({ id }) => id === currentTab); - const { - stickers = recentStickers, - title: packTitle = 'Recent Stickers', - } = selectedPack || {}; - - const [isUsingKeyboard, setIsUsingKeyboard] = React.useState(false); - const [packsPage, setPacksPage] = React.useState(0); - const onClickPrevPackPage = React.useCallback(() => { - setPacksPage(i => i - 1); - }, [setPacksPage]); - const onClickNextPackPage = React.useCallback(() => { - setPacksPage(i => i + 1); - }, [setPacksPage]); - - // Handle escape key - React.useEffect(() => { - const handler = (event: KeyboardEvent) => { - if (event.key === 'Tab') { - // We do NOT prevent default here to allow Tab to be used normally - - setIsUsingKeyboard(true); - - return; - } - - if (event.key === 'Escape') { - event.stopPropagation(); - event.preventDefault(); - - onClose(); - } - }; - - document.addEventListener('keydown', handler); - - return () => { - document.removeEventListener('keydown', handler); - }; - }, [onClose]); - - // Focus popup on after initial render, restore focus on teardown - const [focusRef] = useRestoreFocus(); - - const hasPacks = packs.length > 0; - const isRecents = hasPacks && currentTab === 'recents'; - const hasTimeStickers = isRecents && onPickTimeSticker; - const isEmpty = stickers.length === 0 && !hasTimeStickers; - const addPackRef = isEmpty ? focusRef : undefined; - const downloadError = - selectedPack && - selectedPack.status === 'error' && - selectedPack.stickerCount !== selectedPack.stickers.length; - const pendingCount = - selectedPack && selectedPack.status === 'pending' - ? selectedPack.stickerCount - stickers.length - : 0; - - const showPendingText = pendingCount > 0; - const showDownloadErrorText = downloadError; - const showEmptyText = !downloadError && isEmpty; - const showText = - showPendingText || showDownloadErrorText || showEmptyText; - const showLongText = showPickerHint; - const analogTime = getAnalogTime(); - - return ( - -
-
-
-
- {hasPacks ? ( - - ))} -
- {!isUsingKeyboard && packsPage > 0 ? ( -
- {onClickAddPack && ( -
-
- {showPickerHint ? ( -
- {i18n('icu:stickers--StickerPicker--Hint')} -
- ) : null} - {!hasPacks ? ( -
- {i18n('icu:stickers--StickerPicker--NoPacks')} -
- ) : null} - {pendingCount > 0 ? ( -
- {i18n('icu:stickers--StickerPicker--DownloadPending')} -
- ) : null} - {downloadError ? ( -
- {stickers.length > 0 - ? i18n('icu:stickers--StickerPicker--DownloadError') - : i18n('icu:stickers--StickerPicker--Empty')} -
- ) : null} - {hasPacks && showEmptyText ? ( -
- {isRecents - ? i18n('icu:stickers--StickerPicker--NoRecents') - : i18n('icu:stickers--StickerPicker--Empty')} -
- ) : null} - {!isEmpty ? ( -
- {isRecents && onPickTimeSticker && ( -
- - {i18n('icu:stickers__StickerPicker__featured')} - -
- - - -
- {stickers.length > 0 && ( - - {i18n('icu:stickers__StickerPicker__recent')} - - )} -
- )} -
- {stickers.map(({ packId, id, url }, index: number) => { - const maybeFocusRef = index === 0 ? focusRef : undefined; - - return ( - - ); - })} - {Array(pendingCount) - .fill(0) - .map((_, i) => ( -
- ))} -
-
- ) : null} -
-
- - ); - } - ) -); diff --git a/ts/components/stickers/lib.ts b/ts/components/stickers/lib.ts deleted file mode 100644 index 734f9fb50b..0000000000 --- a/ts/components/stickers/lib.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2019 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import type { StickerPackType } from '../../state/ducks/stickers.js'; - -// This function exists to force stickers to be counted consistently wherever -// they are counted (TypeScript ensures that all data is named and provided) -export function countStickers(o: { - knownPacks: ReadonlyArray; - blessedPacks: ReadonlyArray; - installedPacks: ReadonlyArray; - receivedPacks: ReadonlyArray; -}): number { - return ( - o.knownPacks.length + - o.blessedPacks.length + - o.installedPacks.length + - o.receivedPacks.length - ); -} diff --git a/ts/quill/auto-substitute-ascii-emojis/index.tsx b/ts/quill/auto-substitute-ascii-emojis/index.tsx index 0d8205993e..c167e57474 100644 --- a/ts/quill/auto-substitute-ascii-emojis/index.tsx +++ b/ts/quill/auto-substitute-ascii-emojis/index.tsx @@ -6,12 +6,12 @@ import Emitter from '@signalapp/quill-cjs/core/emitter.js'; import type Quill from '@signalapp/quill-cjs'; import { createLogger } from '../../logging/log.js'; -import type { EmojiData } from '../../components/emoji/lib.js'; +import type { EmojiParentKey } from '../../components/fun/data/emojis.js'; import { - convertShortName, - convertShortNameToData, -} from '../../components/emoji/lib.js'; -import { EmojiSkinTone } from '../../components/fun/data/emojis.js'; + EMOJI_PARENT_KEY_CONSTANTS, + EmojiSkinTone, + getEmojiVariantByParentKeyAndSkinTone, +} from '../../components/fun/data/emojis.js'; const log = createLogger('index'); @@ -19,26 +19,28 @@ export type AutoSubstituteAsciiEmojisOptions = { emojiSkinToneDefault: EmojiSkinTone | null; }; -const emojiMap: Record = { - ':-)': 'slightly_smiling_face', - ':-(': 'slightly_frowning_face', - ':-D': 'grinning', - ':-*': 'kissing_heart', - ':-P': 'stuck_out_tongue', - ':-p': 'stuck_out_tongue', - ":'(": 'cry', - ':-\\': 'confused', - ':-|': 'neutral_face', - ';-)': 'wink', - '(Y)': '+1', - '(N)': '-1', - '(y)': '+1', - '(n)': '-1', - '<3': 'heart', - '^_^': 'grin', +type EmojiShortcutMap = Partial>; + +const emojiShortcutMap: EmojiShortcutMap = { + ':-)': EMOJI_PARENT_KEY_CONSTANTS.SLIGHTLY_SMILING_FACE, + ':-(': EMOJI_PARENT_KEY_CONSTANTS.SLIGHTLY_FROWNING_FACE, + ':-D': EMOJI_PARENT_KEY_CONSTANTS.GRINNING_FACE, + ':-*': EMOJI_PARENT_KEY_CONSTANTS.FACE_BLOWING_A_KISS, + ':-P': EMOJI_PARENT_KEY_CONSTANTS.FACE_WITH_STUCK_OUT_TONGUE, + ':-p': EMOJI_PARENT_KEY_CONSTANTS.FACE_WITH_STUCK_OUT_TONGUE, + ":'(": EMOJI_PARENT_KEY_CONSTANTS.CRYING_FACE, + ':-\\': EMOJI_PARENT_KEY_CONSTANTS.CONFUSED_FACE, + ':-|': EMOJI_PARENT_KEY_CONSTANTS.NEUTRAL_FACE, + ';-)': EMOJI_PARENT_KEY_CONSTANTS.WINKING_FACE, + '(Y)': EMOJI_PARENT_KEY_CONSTANTS.THUMBS_UP, + '(N)': EMOJI_PARENT_KEY_CONSTANTS.THUMBS_UP, + '(y)': EMOJI_PARENT_KEY_CONSTANTS.THUMBS_UP, + '(n)': EMOJI_PARENT_KEY_CONSTANTS.THUMBS_DOWN, + '<3': EMOJI_PARENT_KEY_CONSTANTS.RED_HEART, + '^_^': EMOJI_PARENT_KEY_CONSTANTS.GRINNING_FACE, }; -function buildRegexp(obj: Record): RegExp { +function buildRegexp(obj: EmojiShortcutMap): RegExp { const sanitizedKeys = Object.keys(obj).map(x => x.replace(/([^a-zA-Z0-9])/g, '\\$1') ); @@ -46,7 +48,7 @@ function buildRegexp(obj: Record): RegExp { return new RegExp(`(${sanitizedKeys.join('|')})$`); } -const EMOJI_REGEXP = buildRegexp(emojiMap); +const EMOJI_REGEXP = buildRegexp(emojiShortcutMap); export class AutoSubstituteAsciiEmojis { options: AutoSubstituteAsciiEmojisOptions; @@ -101,15 +103,11 @@ export class AutoSubstituteAsciiEmojis { } const [, textEmoji] = match; - const emojiName = emojiMap[textEmoji]; + const emojiParentKey = emojiShortcutMap[textEmoji]; - const emojiData = convertShortNameToData( - emojiName, - this.options.emojiSkinToneDefault ?? EmojiSkinTone.None - ); - if (emojiData) { + if (emojiParentKey != null) { this.insertEmoji( - emojiData, + emojiParentKey, range.index - textEmoji.length, textEmoji.length, textEmoji @@ -118,20 +116,20 @@ export class AutoSubstituteAsciiEmojis { } insertEmoji( - emojiData: EmojiData, + emojiParentKey: EmojiParentKey, index: number, range: number, source: string ): void { - const emoji = convertShortName( - emojiData.short_name, + const emojiVariant = getEmojiVariantByParentKeyAndSkinTone( + emojiParentKey, this.options.emojiSkinToneDefault ?? EmojiSkinTone.None ); const delta = new Delta() .retain(index) .delete(range) .insert({ - emoji: { value: emoji, source }, + emoji: { value: emojiVariant.value, source }, }); this.quill.updateContents(delta, 'api'); this.quill.setSelection(index + 1, 0); diff --git a/ts/quill/emoji/completion.tsx b/ts/quill/emoji/completion.tsx index 95eb375f80..f16b089961 100644 --- a/ts/quill/emoji/completion.tsx +++ b/ts/quill/emoji/completion.tsx @@ -10,15 +10,13 @@ import { Popper } from 'react-popper'; import classNames from 'classnames'; import { createPortal } from 'react-dom'; import type { VirtualElement } from '@popperjs/core'; -import { convertShortName } from '../../components/emoji/lib.js'; -import type { EmojiPickDataType } from '../../components/emoji/EmojiPicker.js'; import { getBlotTextPartitions, matchBlotTextPartitions } from '../util.js'; import { handleOutsideClick } from '../../util/handleOutsideClick.js'; import { createLogger } from '../../logging/log.js'; import { FunStaticEmoji } from '../../components/fun/FunEmoji.js'; +import type { EmojiParentKey } from '../../components/fun/data/emojis.js'; import { EmojiSkinTone, - getEmojiParentByKey, getEmojiVariantByParentKeyAndSkinTone, normalizeShortNameCompletionDisplay, } from '../../components/fun/data/emojis.js'; @@ -27,13 +25,14 @@ import type { FunEmojiSearch, } from '../../components/fun/useFunEmojiSearch.js'; import { type FunEmojiLocalizer } from '../../components/fun/useFunEmojiLocalizer.js'; +import type { FunEmojiSelection } from '../../components/fun/panels/FunPanelEmojis.js'; const { isNumber, debounce } = lodash; const log = createLogger('completion'); export type EmojiCompletionOptions = { - onPickEmoji: (emoji: EmojiPickDataType) => void; + onSelectEmoji: (emojiSelection: FunEmojiSelection) => void; setEmojiPickerElement: (element: JSX.Element | null) => void; emojiSkinToneDefault: EmojiSkinTone | null; emojiSearch: FunEmojiSearch; @@ -41,7 +40,7 @@ export type EmojiCompletionOptions = { }; export type InsertEmojiOptionsType = Readonly<{ - shortName: string; + emojiParentKey: EmojiParentKey; index: number; range: number; withTrailingSpace?: boolean; @@ -173,10 +172,9 @@ export class EmojiCompletion { this.options.emojiLocalizer.getParentKeyForText(leftTokenText); if (parentKey != null) { const numberOfColons = isSelfClosing ? 2 : 1; - const emoji = getEmojiParentByKey(parentKey); this.insertEmoji({ - shortName: emoji.englishShortNameDefault, + emojiParentKey: parentKey, index: range.index - leftTokenText.length - numberOfColons, range: leftTokenText.length + numberOfColons, justPressedColon, @@ -194,9 +192,8 @@ export class EmojiCompletion { this.options.emojiLocalizer.getParentKeyForText(tokenText); if (parentKey != null) { - const emoji = getEmojiParentByKey(parentKey); this.insertEmoji({ - shortName: emoji.englishShortNameDefault, + emojiParentKey: parentKey, index: range.index - leftTokenText.length - 1, range: tokenText.length + 2, justPressedColon, @@ -252,10 +249,9 @@ export class EmojiCompletion { } const [, tokenText] = tokenTextMatch; - const parent = getEmojiParentByKey(result.parentKey); this.insertEmoji({ - shortName: parent.englishShortNameDefault, + emojiParentKey: result.parentKey, index: range.index - tokenText.length - 1, range: tokenText.length + 1, withTrailingSpace: true, @@ -263,15 +259,16 @@ export class EmojiCompletion { } insertEmoji({ - shortName, + emojiParentKey, index, range, withTrailingSpace = false, justPressedColon = false, }: InsertEmojiOptionsType): void { - const emoji = convertShortName( - shortName, - this.options.emojiSkinToneDefault ?? EmojiSkinTone.None + const skinTone = this.options.emojiSkinToneDefault ?? EmojiSkinTone.None; + const emojiVariant = getEmojiVariantByParentKeyAndSkinTone( + emojiParentKey, + skinTone ); let source = this.quill.getText(index, range); @@ -283,7 +280,7 @@ export class EmojiCompletion { .retain(index) .delete(range) .insert({ - emoji: { value: emoji, source }, + emoji: { value: emojiVariant.value, source }, }); if (withTrailingSpace) { @@ -296,9 +293,8 @@ export class EmojiCompletion { this.quill.setSelection(index + 1, 0, 'user'); } - this.options.onPickEmoji({ - shortName, - skinTone: this.options.emojiSkinToneDefault ?? EmojiSkinTone.None, + this.options.onSelectEmoji({ + variantKey: emojiVariant.key, }); this.reset(); diff --git a/ts/quill/util.ts b/ts/quill/util.ts index f1013bab05..2579e6f441 100644 --- a/ts/quill/util.ts +++ b/ts/quill/util.ts @@ -16,7 +16,11 @@ import type { EmojiBlot } from './emoji/blot.js'; import { isNewlineOnlyOp, QuillFormattingStyle } from './formatting/menu.js'; import { isNotNil } from '../util/isNotNil.js'; import type { AciString } from '../types/ServiceId.js'; -import { emojiToData } from '../components/emoji/lib.js'; +import { + getEmojiVariantByKey, + getEmojiVariantKeyByValue, + isEmojiVariantValue, +} from '../components/fun/data/emojis.js'; export type Matcher = ( node: HTMLElement, @@ -458,15 +462,17 @@ export const insertEmojiOps = ( // eslint-disable-next-line no-cond-assign while ((match = re.exec(text))) { - const [emoji] = match; - const emojiData = emojiToData(emoji); - if (emojiData) { + const [emojiMatch] = match; + if (isEmojiVariantValue(emojiMatch)) { + const variantKey = getEmojiVariantKeyByValue(emojiMatch); + const variant = getEmojiVariantByKey(variantKey); + ops.push({ insert: text.slice(index, match.index), attributes }); ops.push({ - insert: { emoji: { value: emoji } }, + insert: { emoji: { value: variant.value } }, attributes: { ...existingAttributes, ...attributes }, }); - index = match.index + emoji.length; + index = match.index + variant.value.length; } } diff --git a/ts/reactions/constants.ts b/ts/reactions/constants.ts index d28382aa0d..edca4dd5e7 100644 --- a/ts/reactions/constants.ts +++ b/ts/reactions/constants.ts @@ -1,13 +1,15 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -export const DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES = [ - 'heart', - 'thumbsup', - 'thumbsdown', - 'joy', - 'open_mouth', - 'cry', +import { EMOJI_PARENT_KEY_CONSTANTS } from '../components/fun/data/emojis.js'; + +export const DEFAULT_PREFERRED_REACTION_EMOJI_PARENT_KEYS = [ + EMOJI_PARENT_KEY_CONSTANTS.RED_HEART, + EMOJI_PARENT_KEY_CONSTANTS.THUMBS_UP, + EMOJI_PARENT_KEY_CONSTANTS.THUMBS_DOWN, + EMOJI_PARENT_KEY_CONSTANTS.FACE_WITH_TEARS_OF_JOY, + EMOJI_PARENT_KEY_CONSTANTS.FACE_WITH_OPEN_MOUTH, + EMOJI_PARENT_KEY_CONSTANTS.CRYING_FACE, ]; // This is used in storybook for simplicity. Normally we prefer to convert emoji short diff --git a/ts/reactions/preferredReactionEmoji.ts b/ts/reactions/preferredReactionEmoji.ts index e4a5ae16ef..8e03213136 100644 --- a/ts/reactions/preferredReactionEmoji.ts +++ b/ts/reactions/preferredReactionEmoji.ts @@ -3,10 +3,12 @@ import lodash from 'lodash'; import { createLogger } from '../logging/log.js'; -import { DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES } from './constants.js'; -import { convertShortName } from '../components/emoji/lib.js'; +import { DEFAULT_PREFERRED_REACTION_EMOJI_PARENT_KEYS } from './constants.js'; import { isValidReactionEmoji } from './isValidReactionEmoji.js'; -import type { EmojiSkinTone } from '../components/fun/data/emojis.js'; +import { + getEmojiVariantByParentKeyAndSkinTone, + type EmojiSkinTone, +} from '../components/fun/data/emojis.js'; const { times } = lodash; @@ -16,7 +18,7 @@ const MAX_STORED_LENGTH = 20; const MAX_ITEM_LENGTH = 20; const PREFERRED_REACTION_EMOJI_COUNT = - DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES.length; + DEFAULT_PREFERRED_REACTION_EMOJI_PARENT_KEYS.length; export function getPreferredReactionEmoji( storedValue: unknown, @@ -32,27 +34,27 @@ export function getPreferredReactionEmoji( return storedItem; } - const fallbackShortName: undefined | string = - DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES[index]; - if (!fallbackShortName) { + const fallbackParentKey = + DEFAULT_PREFERRED_REACTION_EMOJI_PARENT_KEYS.at(index); + if (fallbackParentKey == null) { log.error( 'Index is out of range. Is the preferred count larger than the list of fallbacks?' ); return '❤️'; } - const fallbackEmoji = convertShortName( - fallbackShortName, + const fallbackEmoji = getEmojiVariantByParentKeyAndSkinTone( + fallbackParentKey, emojiSkinToneDefault ); - if (!fallbackEmoji) { + if (fallbackEmoji == null) { log.error( 'No fallback emoji. Does the fallback list contain an invalid short name?' ); return '❤️'; } - return fallbackEmoji; + return fallbackEmoji.value; }); } diff --git a/ts/state/ducks/emojis.ts b/ts/state/ducks/emojis.ts index a104c1e802..334108ff73 100644 --- a/ts/state/ducks/emojis.ts +++ b/ts/state/ducks/emojis.ts @@ -4,10 +4,14 @@ import lodash from 'lodash'; import type { ThunkAction } from 'redux-thunk'; import type { ReadonlyDeep } from 'type-fest'; -import type { EmojiPickDataType } from '../../components/emoji/EmojiPicker.js'; import { DataWriter } from '../../sql/Client.js'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions.js'; import { useBoundActions } from '../../hooks/useBoundActions.js'; +import type { FunEmojiSelection } from '../../components/fun/panels/FunPanelEmojis.js'; +import { + getEmojiParentByKey, + getEmojiParentKeyByVariantKey, +} from '../../components/fun/data/emojis.js'; const { take, uniq } = lodash; @@ -39,11 +43,16 @@ export const useEmojisActions = (): BoundActionCreatorsMapObject< typeof actions > => useBoundActions(actions); -function onUseEmoji({ - shortName, -}: EmojiPickDataType): ThunkAction { +function onUseEmoji( + emojiSelection: FunEmojiSelection +): ThunkAction { return async dispatch => { try { + const emojiParentKey = getEmojiParentKeyByVariantKey( + emojiSelection.variantKey + ); + const emojiParent = getEmojiParentByKey(emojiParentKey); + const shortName = emojiParent.englishShortNameDefault; await updateEmojiUsage(shortName); dispatch(useEmoji(shortName)); } catch (err) { diff --git a/ts/state/ducks/preferredReactions.ts b/ts/state/ducks/preferredReactions.ts index 8b7941cbfe..83d8fc9bde 100644 --- a/ts/state/ducks/preferredReactions.ts +++ b/ts/state/ducks/preferredReactions.ts @@ -10,11 +10,13 @@ import { replaceIndex } from '../../util/replaceIndex.js'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions.js'; import { useBoundActions } from '../../hooks/useBoundActions.js'; import type { StateType as RootStateType } from '../reducer.js'; -import { DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES } from '../../reactions/constants.js'; +import { DEFAULT_PREFERRED_REACTION_EMOJI_PARENT_KEYS } from '../../reactions/constants.js'; import { getPreferredReactionEmoji } from '../../reactions/preferredReactionEmoji.js'; import { getEmojiSkinToneDefault } from '../selectors/items.js'; -import { convertShortName } from '../../components/emoji/lib.js'; -import { EmojiSkinTone } from '../../components/fun/data/emojis.js'; +import { + EmojiSkinTone, + getEmojiVariantByParentKeyAndSkinTone, +} from '../../components/fun/data/emojis.js'; const { omit } = lodash; @@ -295,9 +297,13 @@ export function reducer( customizePreferredReactionsModal: { ...state.customizePreferredReactionsModal, draftPreferredReactions: - DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES.map(shortName => - convertShortName(shortName, emojiSkinTone) - ), + DEFAULT_PREFERRED_REACTION_EMOJI_PARENT_KEYS.map(parentKey => { + const variant = getEmojiVariantByParentKeyAndSkinTone( + parentKey, + emojiSkinTone + ); + return variant.value; + }), selectedDraftEmojiIndex: undefined, }, }; diff --git a/ts/state/smart/CallManager.tsx b/ts/state/smart/CallManager.tsx index dbca2911a5..6941075a7b 100644 --- a/ts/state/smart/CallManager.tsx +++ b/ts/state/smart/CallManager.tsx @@ -46,7 +46,6 @@ import { import { getConversationSelector, getMe } from '../selectors/conversations.js'; import { getIntl, getUserACI } from '../selectors/user.js'; import { SmartCallingDeviceSelection } from './CallingDeviceSelection.js'; -import { renderEmojiPicker } from './renderEmojiPicker.js'; import { renderReactionPicker } from './renderReactionPicker.js'; import { isSharingPhoneNumberWithEverybody as getIsSharingPhoneNumberWithEverybody } from '../../util/phoneNumberSharingMode.js'; import { useGlobalModalActions } from '../ducks/globalModals.js'; @@ -465,7 +464,6 @@ export const SmartCallManager = memo(function SmartCallManager() { playRingtone={playRingtone} removeClient={removeClient} renderDeviceSelection={renderDeviceSelection} - renderEmojiPicker={renderEmojiPicker} renderReactionPicker={renderReactionPicker} ringingCall={ringingCall} sendGroupCallRaiseHand={sendGroupCallRaiseHand} diff --git a/ts/state/smart/CompositionArea.tsx b/ts/state/smart/CompositionArea.tsx index 27c8d8614c..e931c1b686 100644 --- a/ts/state/smart/CompositionArea.tsx +++ b/ts/state/smart/CompositionArea.tsx @@ -30,51 +30,34 @@ import { getSelectedMessageIds, isMissingRequiredProfileSharing, } from '../selectors/conversations.js'; -import { selectRecentEmojis } from '../selectors/emojis.js'; import { getDefaultConversationColor, getEmojiSkinToneDefault, - getShowStickerPickerHint, - getShowStickersIntroduction, getTextFormattingEnabled, } from '../selectors/items.js'; import { canForward, getPropsForQuote } from '../selectors/message.js'; -import { - getBlessedStickerPacks, - getInstalledStickerPacks, - getKnownStickerPacks, - getReceivedStickerPacks, - getRecentStickers, - getRecentlyInstalledStickerPack, -} from '../selectors/stickers.js'; import { getIntl, getPlatform, getTheme, getUserConversationId, } from '../selectors/user.js'; -import type { SmartCompositionRecordingProps } from './CompositionRecording.js'; import { SmartCompositionRecording } from './CompositionRecording.js'; import type { SmartCompositionRecordingDraftProps } from './CompositionRecordingDraft.js'; import { SmartCompositionRecordingDraft } from './CompositionRecordingDraft.js'; -import { useItemsActions } from '../ducks/items.js'; import { useComposerActions } from '../ducks/composer.js'; import { useConversationsActions } from '../ducks/conversations.js'; import { useAudioRecorderActions } from '../ducks/audioRecorder.js'; import { useEmojisActions } from '../ducks/emojis.js'; import { useGlobalModalActions } from '../ducks/globalModals.js'; -import { useStickersActions } from '../ducks/stickers.js'; import { useToastActions } from '../ducks/toast.js'; import { isShowingAnyModal } from '../selectors/globalModals.js'; import { isConversationEverUnregistered } from '../../util/isConversationUnregistered.js'; import { isDirectConversation } from '../../util/whatTypeOfConversation.js'; import { isConversationMuted } from '../../util/isConversationMuted.js'; -import type { EmojiSkinTone } from '../../components/fun/data/emojis.js'; -function renderSmartCompositionRecording( - recProps: SmartCompositionRecordingProps -) { - return ; +function renderSmartCompositionRecording() { + return ; } function renderSmartCompositionRecordingDraft( @@ -95,21 +78,12 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({ const i18n = useSelector(getIntl); const theme = useSelector(getTheme); const emojiSkinToneDefault = useSelector(getEmojiSkinToneDefault); - const recentEmojis = useSelector(selectRecentEmojis); const selectedMessageIds = useSelector(getSelectedMessageIds); const messageLookup = useSelector(getMessages); const isFormattingEnabled = useSelector(getTextFormattingEnabled); const lastEditableMessageId = useSelector(getLastEditableMessageId); - const receivedPacks = useSelector(getReceivedStickerPacks); - const installedPacks = useSelector(getInstalledStickerPacks); - const blessedPacks = useSelector(getBlessedStickerPacks); - const knownPacks = useSelector(getKnownStickerPacks); const platform = useSelector(getPlatform); const shouldHidePopovers = useSelector(getHasPanelOpen); - const installedPack = useSelector(getRecentlyInstalledStickerPack); - const recentStickers = useSelector(getRecentStickers); - const showStickersIntroduction = useSelector(getShowStickersIntroduction); - const showStickerPickerHint = useSelector(getShowStickerPickerHint); const recordingState = useSelector(getRecordingState); const errorDialogAudioRecorderType = useSelector( getErrorDialogAudioRecorderType @@ -205,23 +179,6 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({ defaultConversationColor, ]); - const { putItem, removeItem } = useItemsActions(); - - const onEmojiSkinToneDefaultChange = useCallback( - (emojiSkinTone: EmojiSkinTone) => { - putItem('emojiSkinToneDefault', emojiSkinTone); - }, - [putItem] - ); - - const clearShowIntroduction = useCallback(() => { - removeItem('showStickersIntroduction'); - }, [removeItem]); - - const clearShowPickerHint = useCallback(() => { - removeItem('showStickerPickerHint'); - }, [removeItem]); - const { onTextTooLong, onCloseLinkPreview, @@ -259,7 +216,6 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({ toggleForwardMessagesModal, toggleDraftGifMessageSendModal, } = useGlobalModalActions(); - const { clearInstalledStickerPack } = useStickersActions(); const { showToast } = useToastActions(); const { onEditorStateChange } = useComposerActions(); @@ -318,19 +274,9 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({ quotedMessageAuthorAci={quotedMessage?.quote?.authorAci ?? null} quotedMessageSentAt={quotedMessage?.quote?.id ?? null} setQuoteByMessageId={setQuoteByMessageId} - // Emojis - recentEmojis={recentEmojis} + // Fun Picker emojiSkinToneDefault={emojiSkinToneDefault} - onPickEmoji={onUseEmoji} - // Stickers - receivedPacks={receivedPacks} - installedPack={installedPack} - blessedPacks={blessedPacks} - knownPacks={knownPacks} - installedPacks={installedPacks} - recentStickers={recentStickers} - showIntroduction={showStickersIntroduction} - showPickerHint={showStickerPickerHint} + onSelectEmoji={onUseEmoji} // Message Requests acceptedMessageRequest={conversation.acceptedMessageRequest ?? null} removalStage={conversation.removalStage ?? null} @@ -385,10 +331,6 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({ // DraftGifMessageSendModal toggleDraftGifMessageSendModal={toggleDraftGifMessageSendModal} // Dispatch - onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange} - clearShowIntroduction={clearShowIntroduction} - clearInstalledStickerPack={clearInstalledStickerPack} - clearShowPickerHint={clearShowPickerHint} showToast={showToast} sendStickerMessage={sendStickerMessage} sendEditedMessage={sendEditedMessage} diff --git a/ts/state/smart/CompositionRecording.tsx b/ts/state/smart/CompositionRecording.tsx index 0c6abb7d05..001b6c5742 100644 --- a/ts/state/smart/CompositionRecording.tsx +++ b/ts/state/smart/CompositionRecording.tsx @@ -10,14 +10,8 @@ import { useToastActions } from '../ducks/toast.js'; import { getSelectedConversationId } from '../selectors/conversations.js'; import { getIntl } from '../selectors/user.js'; -export type SmartCompositionRecordingProps = { - onBeforeSend: () => void; -}; - export const SmartCompositionRecording = memo( - function SmartCompositionRecording({ - onBeforeSend, - }: SmartCompositionRecordingProps) { + function SmartCompositionRecording() { const i18n = useSelector(getIntl); const selectedConversationId = useSelector(getSelectedConversationId); const { errorRecording, cancelRecording, completeRecording } = @@ -34,18 +28,12 @@ export const SmartCompositionRecording = memo( const handleSend = useCallback(() => { if (selectedConversationId) { completeRecording(selectedConversationId, voiceNoteAttachment => { - onBeforeSend(); sendMultiMediaMessage(selectedConversationId, { voiceNoteAttachment, }); }); } - }, [ - selectedConversationId, - completeRecording, - onBeforeSend, - sendMultiMediaMessage, - ]); + }, [selectedConversationId, completeRecording, sendMultiMediaMessage]); if (!selectedConversationId) { return null; diff --git a/ts/state/smart/CompositionTextArea.tsx b/ts/state/smart/CompositionTextArea.tsx index 14d2dbabe5..058aef63a5 100644 --- a/ts/state/smart/CompositionTextArea.tsx +++ b/ts/state/smart/CompositionTextArea.tsx @@ -38,7 +38,7 @@ export const SmartCompositionTextArea = memo(function SmartCompositionTextArea( const platform = useSelector(getPlatform); const ourConversationId = useSelector(getUserConversationId); - const { onUseEmoji: onPickEmoji } = useEmojiActions(); + const { onUseEmoji } = useEmojiActions(); const { setEmojiSkinToneDefault } = useItemsActions(); const { onTextTooLong } = useComposerActions(); @@ -53,7 +53,7 @@ export const SmartCompositionTextArea = memo(function SmartCompositionTextArea( i18n={i18n} isActive isFormattingEnabled={isFormattingEnabled} - onPickEmoji={onPickEmoji} + onSelectEmoji={onUseEmoji} onEmojiSkinToneDefaultChange={setEmojiSkinToneDefault} onTextTooLong={onTextTooLong} platform={platform} diff --git a/ts/state/smart/EmojiPicker.tsx b/ts/state/smart/EmojiPicker.tsx deleted file mode 100644 index ed80245db1..0000000000 --- a/ts/state/smart/EmojiPicker.tsx +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React, { useCallback, forwardRef, memo } from 'react'; -import { useSelector } from 'react-redux'; -import { useRecentEmojis } from '../selectors/emojis.js'; -import { useEmojisActions as useEmojiActions } from '../ducks/emojis.js'; -import type { - EmojiPickDataType, - Props as EmojiPickerProps, -} from '../../components/emoji/EmojiPicker.js'; -import { EmojiPicker } from '../../components/emoji/EmojiPicker.js'; -import { getIntl } from '../selectors/user.js'; -import { getEmojiSkinToneDefault } from '../selectors/items.js'; -import { EmojiSkinTone } from '../../components/fun/data/emojis.js'; - -export const SmartEmojiPicker = memo( - forwardRef< - HTMLDivElement, - Pick< - EmojiPickerProps, - | 'onClickSettings' - | 'onPickEmoji' - | 'onEmojiSkinToneDefaultChange' - | 'onClose' - | 'style' - > - >(function SmartEmojiPickerInner( - { - onClickSettings, - onPickEmoji, - onEmojiSkinToneDefaultChange, - onClose, - style, - }, - ref - ) { - const i18n = useSelector(getIntl); - const emojiSkinToneDefault = useSelector(getEmojiSkinToneDefault); - - const recentEmojis = useRecentEmojis(); - const { onUseEmoji } = useEmojiActions(); - - const handlePickEmoji = useCallback( - (data: EmojiPickDataType) => { - onUseEmoji({ shortName: data.shortName, skinTone: EmojiSkinTone.None }); - onPickEmoji(data); - }, - [onUseEmoji, onPickEmoji] - ); - - return ( - - ); - }) -); diff --git a/ts/state/smart/FunProvider.tsx b/ts/state/smart/FunProvider.tsx index fdd2443dda..2524fe9112 100644 --- a/ts/state/smart/FunProvider.tsx +++ b/ts/state/smart/FunProvider.tsx @@ -83,10 +83,7 @@ export const SmartFunProvider = memo(function SmartFunProvider( const handleSelectEmoji = useCallback( (emojiSelection: FunEmojiSelection) => { - onUseEmoji({ - shortName: emojiSelection.englishShortName, - skinTone: emojiSelection.skinTone, - }); + onUseEmoji(emojiSelection); }, [onUseEmoji] ); diff --git a/ts/state/smart/ProfileEditor.tsx b/ts/state/smart/ProfileEditor.tsx index 78ae1c2bf6..d135b9a5ed 100644 --- a/ts/state/smart/ProfileEditor.tsx +++ b/ts/state/smart/ProfileEditor.tsx @@ -7,11 +7,9 @@ import type { MutableRefObject } from 'react'; import { ProfileEditor } from '../../components/ProfileEditor.js'; import { useConversationsActions } from '../ducks/conversations.js'; -import { useItemsActions } from '../ducks/items.js'; import { useToastActions } from '../ducks/toast.js'; import { useUsernameActions } from '../ducks/username.js'; import { getMe, getProfileUpdateError } from '../selectors/conversations.js'; -import { selectRecentEmojis } from '../selectors/emojis.js'; import { getEmojiSkinToneDefault, getHasCompletedUsernameLinkOnboarding, @@ -58,7 +56,6 @@ export const SmartProfileEditor = memo(function SmartProfileEditor(props: { getHasCompletedUsernameLinkOnboarding ); const hasError = useSelector(getProfileUpdateError); - const recentEmojis = useSelector(selectRecentEmojis); const emojiSkinToneDefault = useSelector(getEmojiSkinToneDefault); const usernameCorrupted = useSelector(getUsernameCorrupted); const usernameEditState = useSelector(getUsernameEditState); @@ -84,7 +81,6 @@ export const SmartProfileEditor = memo(function SmartProfileEditor(props: { deleteUsername, } = useUsernameActions(); const { showToast } = useToastActions(); - const { setEmojiSkinToneDefault } = useItemsActions(); const { changeLocation } = useNavActions(); let errorDialog: JSX.Element | undefined; @@ -139,10 +135,8 @@ export const SmartProfileEditor = memo(function SmartProfileEditor(props: { markCompletedUsernameLinkOnboarding } onProfileChanged={myProfileChanged} - onEmojiSkinToneDefaultChange={setEmojiSkinToneDefault} openUsernameReservationModal={openUsernameReservationModal} profileAvatarUrl={profileAvatarUrl} - recentEmojis={recentEmojis} renderUsernameEditor={renderUsernameEditor} replaceAvatar={replaceAvatar} resetUsernameLink={resetUsernameLink} diff --git a/ts/state/smart/ReactionPicker.tsx b/ts/state/smart/ReactionPicker.tsx index 04d60101dd..39accde6c3 100644 --- a/ts/state/smart/ReactionPicker.tsx +++ b/ts/state/smart/ReactionPicker.tsx @@ -4,8 +4,6 @@ import type { Ref } from 'react'; import React, { forwardRef, memo } from 'react'; import { useSelector } from 'react-redux'; -import { usePreferredReactionsActions } from '../ducks/preferredReactions.js'; -import { useItemsActions } from '../ducks/items.js'; import { getIntl } from '../selectors/user.js'; import { getPreferredReactionEmoji } from '../selectors/items.js'; import type { Props as InternalProps } from '../../components/conversation/ReactionPicker.js'; @@ -26,21 +24,12 @@ export const SmartReactionPicker = memo( props: ExternalProps, ref: Ref ) { - const { openCustomizePreferredReactionsModal } = - usePreferredReactionsActions(); - - const { setEmojiSkinToneDefault } = useItemsActions(); - const i18n = useSelector(getIntl); const preferredReactionEmoji = useSelector(getPreferredReactionEmoji); return ( { - return ( - countStickers({ - knownPacks, - blessedPacks, - installedPacks, - receivedPacks, - }) > 0 - ); - }, [blessedPacks, installedPacks, knownPacks, receivedPacks]); - return ( - ); -} diff --git a/ts/test-electron/quill/emoji/completion_test.tsx b/ts/test-electron/quill/emoji/completion_test.tsx index d68e8bcf4a..b092d5a370 100644 --- a/ts/test-electron/quill/emoji/completion_test.tsx +++ b/ts/test-electron/quill/emoji/completion_test.tsx @@ -10,9 +10,10 @@ import type { InsertEmojiOptionsType, } from '../../../quill/emoji/completion.js'; import { + EMOJI_VARIANT_KEY_CONSTANTS, EmojiSkinTone, - emojiVariantConstant, getEmojiParentKeyByVariantKey, + getEmojiVariantByKey, } from '../../../components/fun/data/emojis.js'; import { _createFunEmojiSearch, @@ -25,9 +26,13 @@ import { import type { LocaleEmojiListType } from '../../../types/emoji.js'; const EMOJI_VARIANTS = { - SMILE: emojiVariantConstant('\u{1F604}'), - SMILE_CAT: emojiVariantConstant('\u{1F638}'), - FRIEND_SHRIMP: emojiVariantConstant('\u{1F364}'), + SMILE: getEmojiVariantByKey( + EMOJI_VARIANT_KEY_CONSTANTS.GRINNING_FACE_WITH_SMILING_EYES + ), + SMILE_CAT: getEmojiVariantByKey( + EMOJI_VARIANT_KEY_CONSTANTS.GRINNING_CAT_WITH_SMILING_EYES + ), + FRIEND_SHRIMP: getEmojiVariantByKey(EMOJI_VARIANT_KEY_CONSTANTS.FRIED_SHRIMP), } as const; const PARENT_KEYS = { @@ -82,7 +87,7 @@ describe('emojiCompletion', () => { const emojiLocalizer = _createFunEmojiLocalizer(localizerIndex); const options: EmojiCompletionOptions = { - onPickEmoji: sinon.stub(), + onSelectEmoji: sinon.stub(), setEmojiPickerElement: sinon.stub(), emojiSkinToneDefault: EmojiSkinTone.None, emojiSearch, @@ -247,9 +252,9 @@ describe('emojiCompletion', () => { }); it('inserts the emoji at the current cursor position', () => { - const [{ shortName, index, range }] = insertEmojiStub.args[0]; + const [{ emojiParentKey, index, range }] = insertEmojiStub.args[0]; - assert.equal(shortName, 'smile'); + assert.equal(emojiParentKey, PARENT_KEYS.SMILE); assert.equal(index, 0); assert.equal(range, 7); }); @@ -276,9 +281,9 @@ describe('emojiCompletion', () => { }); it('inserts the emoji at the current cursor position', () => { - const [{ shortName, index, range }] = insertEmojiStub.args[0]; + const [{ emojiParentKey, index, range }] = insertEmojiStub.args[0]; - assert.equal(shortName, 'smile'); + assert.equal(emojiParentKey, PARENT_KEYS.SMILE); assert.equal(index, 7); assert.equal(range, 7); }); @@ -336,9 +341,9 @@ describe('emojiCompletion', () => { }); it('inserts the emoji at the current cursor position', () => { - const [{ shortName, index, range }] = insertEmojiStub.args[0]; + const [{ emojiParentKey, index, range }] = insertEmojiStub.args[0]; - assert.equal(shortName, 'smile'); + assert.equal(emojiParentKey, PARENT_KEYS.SMILE); assert.equal(index, 0); assert.equal(range, validEmoji.length); }); @@ -385,9 +390,9 @@ describe('emojiCompletion', () => { }); it('inserts the emoji at the current cursor position', () => { - const [{ shortName, index, range }] = insertEmojiStub.args[0]; + const [{ emojiParentKey, index, range }] = insertEmojiStub.args[0]; - assert.equal(shortName, 'smile'); + assert.equal(emojiParentKey, PARENT_KEYS.SMILE); assert.equal(index, 0); assert.equal(range, 6); }); @@ -430,10 +435,10 @@ describe('emojiCompletion', () => { }); it('inserts the currently selected emoji at the current cursor position', () => { - const [{ shortName, index: insertIndex, range }] = + const [{ emojiParentKey, index: insertIndex, range }] = insertEmojiStub.args[0]; - assert.equal(shortName, 'smile_cat'); + assert.equal(emojiParentKey, PARENT_KEYS.SMILE_CAT); assert.equal(insertIndex, 0); assert.equal(range, text.length); }); diff --git a/ts/test-mock/storage/sticker_test.ts b/ts/test-mock/storage/sticker_test.ts index 5936ca6d06..936c8a8428 100644 --- a/ts/test-mock/storage/sticker_test.ts +++ b/ts/test-mock/storage/sticker_test.ts @@ -156,9 +156,23 @@ describe('storage service', function (this: Mocha.Suite) { } debug('opening sticker manager'); - await conversationView - .locator('.CompositionArea .module-sticker-button__button') - .click(); + + const FunButton = window.getByRole('button', { + name: 'Add an Emoji, Sticker, or GIF', + }); + const FunDialog = window.getByRole('dialog', { + name: 'Add an Emoji, Sticker, or GIF', + }); + const FunPickerStickersTab = FunDialog.getByRole('tab', { + name: 'Stickers', + }); + const FunPickerAddSticker = FunDialog.getByRole('button', { + name: 'Add a sticker pack', + }); + + await FunButton.click(); + await FunPickerStickersTab.click(); + await FunPickerAddSticker.click(); const stickerManager = conversationView.locator( '[data-testid=StickerManager]' diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 2a5f12ac07..bffaa2e6e0 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -1162,13 +1162,6 @@ "reasonCategory": "usageTrusted", "updated": "2021-09-23T00:07:11.885Z" }, - { - "rule": "React-useRef", - "path": "ts/components/CompositionArea.tsx", - "line": " const emojiButtonRef = useRef();", - "reasonCategory": "usageTrusted", - "updated": "2022-07-07T20:51:44.602Z" - }, { "rule": "React-useRef", "path": "ts/components/CompositionInput.tsx", @@ -1907,21 +1900,6 @@ "updated": "2024-09-03T00:45:23.978Z", "reasonDetail": "Because we need the current tab value outside the callback" }, - { - "rule": "React-useRef", - "path": "ts/components/emoji/EmojiButton.tsx", - "line": " const buttonRef = React.useRef(null);", - "reasonCategory": "usageTrusted", - "updated": "2022-06-14T22:04:43.988Z", - "reasonDetail": "Handling outside click" - }, - { - "rule": "React-useRef", - "path": "ts/components/emoji/EmojiButton.tsx", - "line": " const popperRef = React.useRef(null);", - "reasonCategory": "usageTrusted", - "updated": "2023-01-18T22:32:43.901Z" - }, { "rule": "React-useRef", "path": "ts/components/fun/FunGif.tsx", @@ -2118,14 +2096,6 @@ "reasonCategory": "usageTrusted", "updated": "2025-07-17T19:33:27.401Z" }, - { - "rule": "React-useRef", - "path": "ts/components/stickers/StickerButton.tsx", - "line": " const buttonRef = React.useRef(null);", - "reasonCategory": "usageTrusted", - "updated": "2022-06-14T22:04:43.988Z", - "reasonDetail": "Handling outside click" - }, { "rule": "React-useRef", "path": "ts/hooks/useConfirmDiscard.tsx",