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 {
|
&__arrow {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
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
|
// 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue