Animated floating emojis

This commit is contained in:
Josh Perez 2022-05-04 13:43:22 -04:00 committed by GitHub
parent 7d8464757b
commit 36c5de4600
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 121 additions and 2 deletions

View file

@ -148,6 +148,13 @@
} }
} }
&__animated-emojis {
height: 100vh;
position: absolute;
width: 100%;
z-index: $z-index-above-base;
}
&__arrow { &__arrow {
align-items: center; align-items: center;
display: flex; display: flex;

View file

@ -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', () => <AnimatedEmojiGalore {...getDefaultProps()} />);

View file

@ -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) => (
<animated.div
// eslint-disable-next-line react/no-array-index-key
key={index}
style={{
left: `${random(0, 100)}%`,
position: 'absolute',
transform: interpolate(
[styles.y, styles.scale, styles.rotate],
transform
),
}}
>
<Emojify sizeClass="extra-large" text={emoji} />
</animated.div>
))}
</>
);
};

View file

@ -1,6 +1,7 @@
// Copyright 2022 Signal Messenger, LLC // Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { ReactNode } from 'react';
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { Blurhash } from 'react-blurhash'; import { Blurhash } from 'react-blurhash';
@ -22,6 +23,7 @@ import { isVideoTypeSupported } from '../util/GoogleChrome';
export type PropsType = { export type PropsType = {
readonly attachment?: AttachmentType; readonly attachment?: AttachmentType;
readonly children?: ReactNode;
readonly i18n: LocalizerType; readonly i18n: LocalizerType;
readonly isPaused?: boolean; readonly isPaused?: boolean;
readonly isThumbnail?: boolean; readonly isThumbnail?: boolean;
@ -33,6 +35,7 @@ export type PropsType = {
export const StoryImage = ({ export const StoryImage = ({
attachment, attachment,
children,
i18n, i18n,
isPaused, isPaused,
isThumbnail, isThumbnail,
@ -142,6 +145,7 @@ export const StoryImage = ({
> >
{storyElement} {storyElement}
{overlay} {overlay}
{children}
</div> </div>
); );
}; };

View file

@ -18,6 +18,7 @@ import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import type { RenderEmojiPickerProps } from './conversation/ReactionPicker'; import type { RenderEmojiPickerProps } from './conversation/ReactionPicker';
import type { ReplyStateType } from '../types/Stories'; import type { ReplyStateType } from '../types/Stories';
import type { StoryViewType } from './StoryListItem'; import type { StoryViewType } from './StoryListItem';
import { AnimatedEmojiGalore } from './AnimatedEmojiGalore';
import { Avatar, AvatarSize } from './Avatar'; import { Avatar, AvatarSize } from './Avatar';
import { ConfirmationDialog } from './ConfirmationDialog'; import { ConfirmationDialog } from './ConfirmationDialog';
import { ContextMenuPopper } from './ContextMenu'; import { ContextMenuPopper } from './ContextMenu';
@ -117,6 +118,7 @@ export const StoryViewer = ({
const [hasConfirmHideStory, setHasConfirmHideStory] = useState(false); const [hasConfirmHideStory, setHasConfirmHideStory] = useState(false);
const [referenceElement, setReferenceElement] = const [referenceElement, setReferenceElement] =
useState<HTMLButtonElement | null>(null); useState<HTMLButtonElement | null>(null);
const [reactionEmoji, setReactionEmoji] = useState<string | undefined>();
const visibleStory = stories[currentStoryIndex]; const visibleStory = stories[currentStoryIndex];
@ -263,7 +265,8 @@ export const StoryViewer = ({
hasConfirmHideStory || hasConfirmHideStory ||
hasExpandedCaption || hasExpandedCaption ||
hasReplyModal || hasReplyModal ||
isShowingContextMenu; isShowingContextMenu ||
Boolean(reactionEmoji);
useEffect(() => { useEffect(() => {
if (shouldPauseViewing) { if (shouldPauseViewing) {
@ -392,7 +395,18 @@ export const StoryViewer = ({
moduleClassName="StoryViewer__story" moduleClassName="StoryViewer__story"
queueStoryDownload={queueStoryDownload} queueStoryDownload={queueStoryDownload}
storyId={messageId} storyId={messageId}
/> >
{reactionEmoji && (
<div className="StoryViewer__animated-emojis">
<AnimatedEmojiGalore
emoji={reactionEmoji}
onAnimationEnd={() => {
setReactionEmoji(undefined);
}}
/>
</div>
)}
</StoryImage>
{hasExpandedCaption && ( {hasExpandedCaption && (
<button <button
aria-label={i18n('close-popup')} aria-label={i18n('close-popup')}
@ -610,6 +624,8 @@ export const StoryViewer = ({
onClose={() => setHasReplyModal(false)} onClose={() => setHasReplyModal(false)}
onReact={emoji => { onReact={emoji => {
onReactToStory(emoji, visibleStory); onReactToStory(emoji, visibleStory);
setHasReplyModal(false);
setReactionEmoji(emoji);
}} }}
onReply={(message, mentions, replyTimestamp) => { onReply={(message, mentions, replyTimestamp) => {
if (!isGroupStory) { if (!isGroupStory) {