diff --git a/_locales/en/messages.json b/_locales/en/messages.json index f8bc016c49d5..e5acee4aa279 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -3136,11 +3136,15 @@ "message": "Please set up Signal on your phone and desktop to use the Sticker Pack Creator", "description": "The error message which appears when the user has not linked their account and attempts to use the Sticker Creator" }, + "Reactions--remove": { + "message": "Remove reaction", + "describe": "Shown when you want to remove a reaction you've made" + }, "Reactions--error": { "message": "Failed to send reaction. Please try again.", "description": "Shown when a reaction fails to send" }, - "ReactionsViewer--more": { + "Reactions--more": { "message": "More", "description": "Use in the reaction picker as the alt text for the 'more' button" }, diff --git a/images/any-emoji-32-dark-hover.svg b/images/any-emoji-32-dark-hover.svg deleted file mode 100644 index 4c3bd9573219..000000000000 --- a/images/any-emoji-32-dark-hover.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/images/any-emoji-32-dark.svg b/images/any-emoji-32-dark.svg deleted file mode 100644 index 0412142328ee..000000000000 --- a/images/any-emoji-32-dark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/images/any-emoji-32-light-hover.svg b/images/any-emoji-32-light-hover.svg deleted file mode 100644 index d87d4f006a86..000000000000 --- a/images/any-emoji-32-light-hover.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/images/any-emoji-32-light.svg b/images/any-emoji-32-light.svg deleted file mode 100644 index c92c061735ea..000000000000 --- a/images/any-emoji-32-light.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 80d067f6fe41..df80b3dd5abe 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -4684,142 +4684,6 @@ button.module-image__border-overlay:focus { } } -// Module: Reaction Picker - -@keyframes module-reaction-picker__background-fade { - from { - background: transparent; - } - to { - // This color is the same in both light and dark themes - background: rgba($color-black, 0.8); - } -} - -@keyframes module-reaction-picker__emoji-fade { - from { - transform: translate3d(0, 24px, 0); - opacity: 0; - } - to { - transform: translate3d(0, 0, 0); - opacity: 1; - } -} - -.module-reaction-picker { - width: 320px; - height: 56px; - border-radius: 30px; - position: relative; - z-index: 2; - - animation: { - name: module-reaction-picker__background-fade; - duration: 400ms; - timing-function: $ease-out-expo; - fill-mode: forwards; - } - - &__emoji-btn { - @include button-reset; - display: flex; - min-width: 52px; - min-height: 52px; - border-radius: 52px; - - position: absolute; - top: 2px; - - @for $i from 0 through 6 { - &:nth-of-type(#{$i + 1}) { - left: 2px + ($i * 44px); - - // Prevent animation jank - opacity: 0; - - animation: { - name: module-reaction-picker__emoji-fade; - duration: 400ms; - timing-function: $ease-out-expo; - delay: #{$i * 10ms}; - fill-mode: forwards; - } - } - } - - transition: background 400ms $ease-out-expo; - &--selected { - // This color is the same in both light and dark themes - background: rgba($color-white, 0.3); - } - &--more { - @include light-theme { - background: url('../images/any-emoji-32-light.svg') no-repeat center; - } - - @include dark-theme { - background: url('../images/any-emoji-32-dark.svg') no-repeat center; - } - - &::after { - content: ''; - display: block; - width: 52px; - height: 52px; - opacity: 0; - transition: opacity 400ms $ease-out-expo; - - @include light-theme { - background: url('../images/any-emoji-32-light-hover.svg') no-repeat - center; - } - - @include dark-theme { - background: url('../images/any-emoji-32-dark-hover.svg') no-repeat - center; - } - } - - &:hover::after { - opacity: 1; - } - } - - @include keyboard-mode { - &:focus:before { - content: ''; - display: block; - width: 4px; - height: 4px; - background: $color-ultramarine; - border-radius: 2px; - position: absolute; - bottom: 4px; - left: calc(50% - 2px); - } - } - - $emoji-btn: &; - - &__emoji { - position: absolute; - left: 2px; - top: 2px; - transform-origin: center; - $scale: 32 / 48; - transform: scale3d($scale, $scale, $scale); - transition: transform 400ms $ease-out-expo; - - #{$emoji-btn}:hover &, - .keyboard-mode #{$emoji-btn}:focus & { - transform: scale3d(1, 1, 1) translate3d(0, -24px, 0); - z-index: 1; - } - } - } -} - // Module: Calling .module-calling { &__container { diff --git a/stylesheets/components/ReactionPicker.scss b/stylesheets/components/ReactionPicker.scss new file mode 100644 index 000000000000..a63ff6a97dda --- /dev/null +++ b/stylesheets/components/ReactionPicker.scss @@ -0,0 +1,182 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.module-ReactionPicker { + $button-size: 40px; + $button-content-size: 28px; + $max-expected-buttons: 7; + + $emoji-size-from-component: 48px; + $big-emoji-size: 42px; + + @include rounded-corners; + align-items: center; + border-style: solid; + border-width: 1px; + box-shadow: 0 1px 4px $color-black-alpha-05, 0 10px 16px $color-black-alpha-20; + display: inline-flex; + flex-direction: row; + padding: 3px 7px; + position: relative; + user-select: none; + z-index: 2; + + @media (prefers-reduced-motion: no-preference) { + animation: { + name: module-ReactionPicker__appear; + duration: 400ms; + timing-function: $ease-out-expo; + fill-mode: forwards; + } + } + + @include light-theme { + background: $color-white; + border-color: $color-black-alpha-05; + } + + @include dark-theme { + background: $color-gray-75; + border-color: $color-gray-80; + } + + &__button { + @include button-reset; + align-items: center; + border-radius: 100%; + display: flex; + justify-content: center; + position: relative; + + @media (prefers-reduced-motion: no-preference) { + // Prevent animation jank + opacity: 0; + + animation: { + name: module-ReactionPicker__button-appear; + duration: 400ms; + timing-function: $ease-out-expo; + fill-mode: forwards; + // This delay is a fallback in case there are more than the expected number of + // buttons. + delay: #{$max-expected-buttons * 10ms}; + } + } + @for $i from 0 through $max-expected-buttons { + &:nth-of-type(#{$i + 1}) { + animation-delay: #{$i * 10ms}; + } + } + + &--emoji { + $emoji-button-selector: &; + + height: $button-size; + width: $button-size; + + .module-emoji { + transform: scale($button-content-size / $emoji-size-from-component); + @media (prefers-reduced-motion: no-preference) { + transition: transform 400ms $ease-out-expo; + } + } + + @mixin focused-emoji { + @media (prefers-reduced-motion: no-preference) { + .module-emoji { + transform: scale($big-emoji-size / $emoji-size-from-component) + translateY(-16px); + } + } + } + + &:hover { + @include focused-emoji; + } + + @include keyboard-mode { + &:focus { + @include focused-emoji; + } + } + } + + &--more { + // The margin makes the button take up the same space as the other buttons, while + // not actually being as large. + height: $button-content-size; + margin: ($button-size - $button-content-size) / 2; + width: $button-content-size; + @media (prefers-reduced-motion: no-preference) { + transition: background 200ms $ease-out-expo; + } + + @include light-theme { + background: $color-gray-02; + + &:hover { + background: $color-gray-05; + } + } + + @include dark-theme { + background: $color-gray-60; + + &:hover { + background: $color-gray-45; + } + } + + &__dot { + border-radius: 100%; + height: 3px; + margin-right: 4px; + width: 3px; + + &:last-child { + margin-right: 0; + } + + @include light-theme { + background: $color-gray-45; + } + + @include dark-theme { + background: $color-gray-15; + } + } + } + + &--selected { + @include light-theme { + background: $color-black-alpha-20; + } + + @include dark-theme { + background: $color-white-alpha-20; + } + } + } +} + +@keyframes module-ReactionPicker__appear { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes module-ReactionPicker__button-appear { + from { + transform: translate3d(0, 24px, 0); + opacity: 0; + } + + to { + transform: translate3d(0, 0, 0); + opacity: 1; + } +} diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index cc0221440a82..c96409a3f22a 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -27,8 +27,8 @@ // New style: components @import './components/AddGroupMembersModal.scss'; -@import './components/App.scss'; @import './components/AnnouncementsOnlyGroupBanner.scss'; +@import './components/App.scss'; @import './components/Avatar.scss'; @import './components/AvatarEditor.scss'; @import './components/AvatarModalButtons.scss'; @@ -69,6 +69,7 @@ @import './components/Modal.scss'; @import './components/Preferences.scss'; @import './components/ProfileEditor.scss'; +@import './components/ReactionPicker.scss'; @import './components/SafetyNumberChangeDialog.scss'; @import './components/SafetyNumberViewer.scss'; @import './components/SearchInput.scss'; diff --git a/ts/components/conversation/ReactionPicker.tsx b/ts/components/conversation/ReactionPicker.tsx index 45f98091c82d..84d2cd3c28a9 100644 --- a/ts/components/conversation/ReactionPicker.tsx +++ b/ts/components/conversation/ReactionPicker.tsx @@ -34,6 +34,34 @@ const DEFAULT_EMOJI_LIST = [ 'cry', ]; +const EmojiButton = React.forwardRef< + HTMLButtonElement, + { + emoji: string; + onSelect: () => unknown; + selected: boolean; + title?: string; + } +>(({ emoji, onSelect, selected, title }, ref) => ( + +)); + export const ReactionPicker = React.forwardRef( ( { i18n, selected, onClose, skinTone, onPick, renderEmojiPicker, style }, @@ -64,70 +92,69 @@ export const ReactionPicker = React.forwardRef( [onPick] ); + // Focus first button and restore focus on unmount + const [focusRef] = useRestoreFocus(); + + if (pickingOther) { + return renderEmojiPicker({ onPickEmoji, onClose, style, ref }); + } + const emojis = DEFAULT_EMOJI_LIST.map(shortName => convertShortName(shortName, skinTone) ); - // Focus first button and restore focus on unmount - const [focusRef] = useRestoreFocus(); - const otherSelected = selected && !emojis.includes(selected); - return pickingOther ? ( - renderEmojiPicker({ onPickEmoji, onClose, style, ref }) - ) : ( -
+ let moreButton: React.ReactNode; + if (otherSelected) { + moreButton = ( + { + onPick(selected); + }} + selected + title={i18n('Reactions--remove')} + /> + ); + } else { + moreButton = ( + + ); + } + + return ( +
{emojis.map((emoji, index) => { const maybeFocusRef = index === 0 ? focusRef : undefined; return ( - + ref={maybeFocusRef} + selected={emoji === selected} + /> ); })} - + {moreButton}
); } diff --git a/ts/components/emoji/Emoji.tsx b/ts/components/emoji/Emoji.tsx index a7f46e8808d3..3e7ae9c95748 100644 --- a/ts/components/emoji/Emoji.tsx +++ b/ts/components/emoji/Emoji.tsx @@ -15,6 +15,7 @@ export type OwnProps = { skinTone?: SkinToneKey | number; size?: EmojiSizeType; children?: React.ReactNode; + title?: string; }; export type Props = OwnProps & @@ -27,7 +28,15 @@ export type Props = OwnProps & export const Emoji = React.memo( React.forwardRef( ( - { style = {}, size = 28, shortName, skinTone, emoji, className }: Props, + { + className, + emoji, + shortName, + size = 28, + skinTone, + style = {}, + title, + }: Props, ref ) => { let image = ''; @@ -50,8 +59,8 @@ export const Emoji = React.memo( );