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 && (