Reaction Selector: Better thumbs

This commit is contained in:
Alexander Zinchuk 2022-01-28 02:11:13 +01:00
parent 616a266cc4
commit 871d83b951
6 changed files with 175 additions and 97 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -10,7 +10,7 @@
right: -3rem;
top: -3.5rem;
.bubble-big {
&__bubble-big {
position: absolute;
display: block;
content: "";
@ -23,7 +23,7 @@
z-index: -1;
}
.bubble-small {
&__bubble-small {
position: absolute;
display: block;
content: "";
@ -40,18 +40,19 @@
box-shadow: 0 0.25rem 0.125rem var(--color-default-shadow);
}
body.is-safari .bubble-small, body.is-safari .bubble-big {
body.is-safari &__bubble-small,
body.is-safari &__bubble-big {
box-shadow: 0 0.25rem 0.125rem var(--color-default-shadow);
}
.items-wrapper {
&__items-wrapper {
width: 100%;
height: 100%;
overflow: hidden;
border-radius: 3rem;
}
.items {
&__items {
padding: 0 1rem;
width: 100%;
height: 100%;
@ -63,29 +64,4 @@
align-items: center;
border-radius: 3rem;
}
.reaction {
margin-left: 0.5rem;
position: relative;
min-width: 2rem;
min-height: 2rem;
&:first-child {
margin-left: 0;
}
}
.ReactionStaticEmoji {
width: 2rem;
position: absolute;
top: 0;
left: 0;
transform: scale(0.9);
}
.AnimatedSticker {
position: absolute;
top: 0;
left: 0;
}
}

View File

@ -2,21 +2,16 @@ import React, {
FC, memo, useLayoutEffect, useRef,
} from '../../../lib/teact/teact';
import { ApiAvailableReaction, ApiMediaFormat } from '../../../api/types';
import { ApiAvailableReaction } from '../../../api/types';
import useMedia from '../../../hooks/useMedia';
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
import useFlag from '../../../hooks/useFlag';
import useShowTransition from '../../../hooks/useShowTransition';
import { getTouchY } from '../../../util/scrollLock';
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
import AnimatedSticker from '../../common/AnimatedSticker';
import { createClassNameBuilder } from '../../../util/buildClassName';
import ReactionSelectorReaction from './ReactionSelectorReaction';
import './ReactionSelector.scss';
const REACTION_SIZE = 32;
type OwnProps = {
enabledReactions?: string[];
onSendReaction: (reaction: string, x: number, y: number) => void;
@ -25,57 +20,8 @@ type OwnProps = {
isReady?: boolean;
};
const AvailableReaction: FC<{
reaction: ApiAvailableReaction;
isReady?: boolean;
onSendReaction: (reaction: string, x: number, y: number) => void;
}> = ({ reaction, onSendReaction, isReady }) => {
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
const cn = createClassNameBuilder('ReactionSelector');
const mediaData = useMedia(`document${reaction.selectAnimation?.id}`, !isReady, ApiMediaFormat.Lottie);
const [isActivated, activate, deactivate] = useFlag();
const [isAnimationLoaded, markAnimationLoaded] = useFlag();
const shouldRenderAnimated = Boolean(isReady && mediaData);
const { transitionClassNames: animatedClassNames } = useShowTransition(shouldRenderAnimated);
const { shouldRender: shouldRenderStatic, transitionClassNames: staticClassNames } = useShowTransition(
!isReady || !isAnimationLoaded, undefined, true,
);
function handleClick() {
if (!containerRef.current) return;
const { x, y } = containerRef.current.getBoundingClientRect();
onSendReaction(reaction.reaction, x, y);
}
return (
<div
className="reaction"
onClick={handleClick}
ref={containerRef}
onMouseEnter={isReady ? activate : undefined}
>
{shouldRenderStatic && (
<ReactionStaticEmoji className={isReady ? staticClassNames : undefined} reaction={reaction.reaction} />
)}
{shouldRenderAnimated && (
<AnimatedSticker
id={`select_${reaction.reaction}`}
className={animatedClassNames}
animationData={mediaData}
play={isActivated}
noLoop
size={REACTION_SIZE}
onLoad={markAnimationLoaded}
onEnded={deactivate}
/>
)}
</div>
);
};
const ReactionSelector: FC<OwnProps> = ({
availableReactions,
enabledReactions,
@ -104,16 +50,16 @@ const ReactionSelector: FC<OwnProps> = ({
if ((!isPrivate && !enabledReactions?.length) || !availableReactions) return undefined;
return (
<div className="ReactionSelector" onWheelCapture={handleWheel} onTouchMove={handleWheel}>
<div className="bubble-big" />
<div className="bubble-small" />
<div className="items-wrapper">
<div className="items no-scrollbar" ref={itemsScrollRef}>
<div className={cn('&')} onWheelCapture={handleWheel} onTouchMove={handleWheel}>
<div className={cn('bubble-big')} />
<div className={cn('bubble-small')} />
<div className={cn('items-wrapper')}>
<div className={cn('items', ['no-scrollbar'])} ref={itemsScrollRef}>
{availableReactions?.map((reaction) => {
if (reaction.isInactive
|| (!isPrivate && (!enabledReactions || !enabledReactions.includes(reaction.reaction)))) return undefined;
return (
<AvailableReaction
<ReactionSelectorReaction
key={reaction.reaction}
isReady={isReady}
onSendReaction={onSendReaction}

View File

@ -0,0 +1,70 @@
.ReactionSelectorReaction {
margin-left: 0.5rem;
position: relative;
min-width: 2rem;
min-height: 2rem;
&:first-child {
margin-left: 0;
}
&__static {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background: url('../../../assets/reaction-thumbs.png') no-repeat;
background-size: auto 100%;
&--reaction-👍 {
background-position-x: 0;
}
&--reaction-👎 {
background-position-x: -32px;
}
&--reaction- {
background-position-x: -64px;
}
&--reaction-🔥 {
background-position-x: -96px;
}
&--reaction-🎉 {
background-position-x: -128px;
}
&--reaction-🤩 {
background-position-x: -160px;
}
&--reaction-😱 {
background-position-x: -192px;
}
&--reaction-😁 {
background-position-x: -224px;
}
&--reaction-😢 {
background-position-x: -256px;
}
&--reaction-💩 {
background-position-x: -288px;
}
&--reaction-🤮 {
background-position-x: -320px;
}
}
.AnimatedSticker {
position: absolute;
top: 0;
left: 0;
}
}

View File

@ -0,0 +1,80 @@
import React, {
FC, memo, useRef,
} from '../../../lib/teact/teact';
import { ApiAvailableReaction, ApiMediaFormat } from '../../../api/types';
import useMedia from '../../../hooks/useMedia';
import useFlag from '../../../hooks/useFlag';
import useShowTransition from '../../../hooks/useShowTransition';
import { createClassNameBuilder } from '../../../util/buildClassName';
import AnimatedSticker from '../../common/AnimatedSticker';
import './ReactionSelectorReaction.scss';
const REACTION_SIZE = 32;
type OwnProps = {
reaction: ApiAvailableReaction;
isReady?: boolean;
onSendReaction: (reaction: string, x: number, y: number) => void;
};
const cn = createClassNameBuilder('ReactionSelectorReaction');
const ReactionSelectorReaction: FC<OwnProps> = ({ reaction, onSendReaction, isReady }) => {
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
const mediaData = useMedia(`document${reaction.selectAnimation?.id}`, !isReady, ApiMediaFormat.Lottie);
const [isActivated, activate, deactivate] = useFlag();
const [isAnimationLoaded, markAnimationLoaded] = useFlag();
const shouldRenderAnimated = Boolean(isReady && mediaData);
const { transitionClassNames: animatedClassNames } = useShowTransition(shouldRenderAnimated);
const { shouldRender: shouldRenderStatic, transitionClassNames: staticClassNames } = useShowTransition(
!isReady || !isAnimationLoaded, undefined, true,
);
function handleClick() {
if (!containerRef.current) return;
const { x, y } = containerRef.current.getBoundingClientRect();
onSendReaction(reaction.reaction, x, y);
}
return (
<div
className={cn('&')}
onClick={handleClick}
ref={containerRef}
onMouseEnter={isReady ? activate : undefined}
>
{shouldRenderStatic && (
<div
className={cn(
'static',
`reaction-${reaction.reaction}`,
isReady ? [staticClassNames] : undefined,
)}
/>
)}
{shouldRenderAnimated && (
<AnimatedSticker
id={`select_${reaction.reaction}`}
className={cn('animated', [animatedClassNames])}
animationData={mediaData}
play={isActivated}
noLoop
size={REACTION_SIZE}
onLoad={markAnimationLoaded}
onEnded={deactivate}
/>
)}
</div>
);
};
export default memo(ReactionSelectorReaction);

View File

@ -1,17 +1,23 @@
type Parts = (string | false | undefined)[];
type PartsWithGlobals = (string | false | undefined | string[])[];
export default function buildClassName(...parts: Parts) {
return parts.filter(Boolean).join(' ');
}
export function createClassNameBuilder(componentName: string) {
return (elementName: string, ...modifiers: Parts) => {
return (elementName: string, ...modifiers: PartsWithGlobals) => {
const baseName = elementName === '&' ? componentName : `${componentName}__${elementName}`;
return modifiers.reduce((acc, modifier) => {
return modifiers.reduce<string[]>((acc, modifier) => {
if (modifier) {
// A bit hacky way to pass global class names
if (Array.isArray(modifier)) {
acc.push(...modifier);
} else {
acc.push(`${baseName}--${modifier}`);
}
}
return acc;
}, [baseName]).join(' ');