diff --git a/stylesheets/components/StoryViewer.scss b/stylesheets/components/StoryViewer.scss index 7f0197c13f3..14249034e35 100644 --- a/stylesheets/components/StoryViewer.scss +++ b/stylesheets/components/StoryViewer.scss @@ -148,6 +148,13 @@ } } + &__animated-emojis { + height: 100vh; + position: absolute; + width: 100%; + z-index: $z-index-above-base; + } + &__arrow { align-items: center; display: flex; diff --git a/ts/components/AnimatedEmojiGalore.stories.tsx b/ts/components/AnimatedEmojiGalore.stories.tsx new file mode 100644 index 00000000000..e9c2f414557 --- /dev/null +++ b/ts/components/AnimatedEmojiGalore.stories.tsx @@ -0,0 +1,20 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; + +import type { PropsType } from './AnimatedEmojiGalore'; +import { AnimatedEmojiGalore } from './AnimatedEmojiGalore'; + +const story = storiesOf('Components/AnimatedEmojiGalore', module); + +function getDefaultProps(): PropsType { + return { + emoji: '❤️', + onAnimationEnd: action('onAnimationEnd'), + }; +} + +story.add('Hearts', () => ); diff --git a/ts/components/AnimatedEmojiGalore.tsx b/ts/components/AnimatedEmojiGalore.tsx new file mode 100644 index 00000000000..24a7a4c43a9 --- /dev/null +++ b/ts/components/AnimatedEmojiGalore.tsx @@ -0,0 +1,72 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; +import { animated, to as interpolate, useSprings } from '@react-spring/web'; +import { random } from 'lodash'; +import { Emojify } from './conversation/Emojify'; + +export type PropsType = { + emoji: string; + onAnimationEnd: () => unknown; + rotate?: number; + scale?: number; + x?: number; +}; + +const NUM_EMOJIS = 16; +const MAX_HEIGHT = 1280; + +const to = (i: number, f: () => unknown) => ({ + delay: i * random(80, 120), + rotate: random(-24, 24), + scale: random(0.5, 1.0, true), + y: -144, + onRest: i === NUM_EMOJIS - 1 ? f : undefined, +}); +const from = (_i: number) => ({ + rotate: 0, + scale: 1, + y: MAX_HEIGHT, +}); + +function transform(y: number, scale: number, rotate: number): string { + return `translateY(${y}px) scale(${scale}) rotate(${rotate}deg)`; +} + +export const AnimatedEmojiGalore = ({ + emoji, + onAnimationEnd, +}: PropsType): JSX.Element => { + const [springs] = useSprings(NUM_EMOJIS, i => ({ + ...to(i, onAnimationEnd), + from: from(i), + config: { + mass: 20, + tension: 120, + friction: 80, + clamp: true, + }, + })); + + return ( + <> + {springs.map((styles, index) => ( + + + + ))} + + ); +}; diff --git a/ts/components/StoryImage.tsx b/ts/components/StoryImage.tsx index bbb20b6ddb8..7174d07ffa7 100644 --- a/ts/components/StoryImage.tsx +++ b/ts/components/StoryImage.tsx @@ -1,6 +1,7 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import type { ReactNode } from 'react'; import React, { useEffect, useRef } from 'react'; import classNames from 'classnames'; import { Blurhash } from 'react-blurhash'; @@ -22,6 +23,7 @@ import { isVideoTypeSupported } from '../util/GoogleChrome'; export type PropsType = { readonly attachment?: AttachmentType; + readonly children?: ReactNode; readonly i18n: LocalizerType; readonly isPaused?: boolean; readonly isThumbnail?: boolean; @@ -33,6 +35,7 @@ export type PropsType = { export const StoryImage = ({ attachment, + children, i18n, isPaused, isThumbnail, @@ -142,6 +145,7 @@ export const StoryImage = ({ > {storyElement} {overlay} + {children} ); }; diff --git a/ts/components/StoryViewer.tsx b/ts/components/StoryViewer.tsx index 3f3f42c98de..72027223dcd 100644 --- a/ts/components/StoryViewer.tsx +++ b/ts/components/StoryViewer.tsx @@ -18,6 +18,7 @@ import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; import type { RenderEmojiPickerProps } from './conversation/ReactionPicker'; import type { ReplyStateType } from '../types/Stories'; import type { StoryViewType } from './StoryListItem'; +import { AnimatedEmojiGalore } from './AnimatedEmojiGalore'; import { Avatar, AvatarSize } from './Avatar'; import { ConfirmationDialog } from './ConfirmationDialog'; import { ContextMenuPopper } from './ContextMenu'; @@ -117,6 +118,7 @@ export const StoryViewer = ({ const [hasConfirmHideStory, setHasConfirmHideStory] = useState(false); const [referenceElement, setReferenceElement] = useState(null); + const [reactionEmoji, setReactionEmoji] = useState(); const visibleStory = stories[currentStoryIndex]; @@ -263,7 +265,8 @@ export const StoryViewer = ({ hasConfirmHideStory || hasExpandedCaption || hasReplyModal || - isShowingContextMenu; + isShowingContextMenu || + Boolean(reactionEmoji); useEffect(() => { if (shouldPauseViewing) { @@ -392,7 +395,18 @@ export const StoryViewer = ({ moduleClassName="StoryViewer__story" queueStoryDownload={queueStoryDownload} storyId={messageId} - /> + > + {reactionEmoji && ( +
+ { + setReactionEmoji(undefined); + }} + /> +
+ )} + {hasExpandedCaption && (