Animated floating emojis
This commit is contained in:
parent
7d8464757b
commit
36c5de4600
5 changed files with 121 additions and 2 deletions
|
@ -148,6 +148,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__animated-emojis {
|
||||
height: 100vh;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: $z-index-above-base;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
|
20
ts/components/AnimatedEmojiGalore.stories.tsx
Normal file
20
ts/components/AnimatedEmojiGalore.stories.tsx
Normal 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()} />);
|
72
ts/components/AnimatedEmojiGalore.tsx
Normal file
72
ts/components/AnimatedEmojiGalore.tsx
Normal 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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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<HTMLButtonElement | null>(null);
|
||||
const [reactionEmoji, setReactionEmoji] = useState<string | undefined>();
|
||||
|
||||
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 && (
|
||||
<div className="StoryViewer__animated-emojis">
|
||||
<AnimatedEmojiGalore
|
||||
emoji={reactionEmoji}
|
||||
onAnimationEnd={() => {
|
||||
setReactionEmoji(undefined);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</StoryImage>
|
||||
{hasExpandedCaption && (
|
||||
<button
|
||||
aria-label={i18n('close-popup')}
|
||||
|
@ -610,6 +624,8 @@ export const StoryViewer = ({
|
|||
onClose={() => setHasReplyModal(false)}
|
||||
onReact={emoji => {
|
||||
onReactToStory(emoji, visibleStory);
|
||||
setHasReplyModal(false);
|
||||
setReactionEmoji(emoji);
|
||||
}}
|
||||
onReply={(message, mentions, replyTimestamp) => {
|
||||
if (!isGroupStory) {
|
||||
|
|
Loading…
Reference in a new issue