mirror of
https://github.com/danog/telegram-tt.git
synced 2025-01-21 21:01:29 +01:00
Reaction Selector: Better thumbs
This commit is contained in:
parent
616a266cc4
commit
871d83b951
BIN
src/assets/reaction-thumbs.png
Normal file
BIN
src/assets/reaction-thumbs.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 53 KiB |
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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}
|
||||
|
70
src/components/middle/message/ReactionSelectorReaction.scss
Normal file
70
src/components/middle/message/ReactionSelectorReaction.scss
Normal 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;
|
||||
}
|
||||
}
|
80
src/components/middle/message/ReactionSelectorReaction.tsx
Normal file
80
src/components/middle/message/ReactionSelectorReaction.tsx
Normal 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);
|
@ -1,16 +1,22 @@
|
||||
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) {
|
||||
acc.push(`${baseName}--${modifier}`);
|
||||
// A bit hacky way to pass global class names
|
||||
if (Array.isArray(modifier)) {
|
||||
acc.push(...modifier);
|
||||
} else {
|
||||
acc.push(`${baseName}--${modifier}`);
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
|
Loading…
x
Reference in New Issue
Block a user