mirror of
https://github.com/danog/telegram-tt.git
synced 2025-01-22 05:11:55 +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;
|
right: -3rem;
|
||||||
top: -3.5rem;
|
top: -3.5rem;
|
||||||
|
|
||||||
.bubble-big {
|
&__bubble-big {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
content: "";
|
content: "";
|
||||||
@ -23,7 +23,7 @@
|
|||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bubble-small {
|
&__bubble-small {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
content: "";
|
content: "";
|
||||||
@ -40,18 +40,19 @@
|
|||||||
box-shadow: 0 0.25rem 0.125rem var(--color-default-shadow);
|
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);
|
box-shadow: 0 0.25rem 0.125rem var(--color-default-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.items-wrapper {
|
&__items-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 3rem;
|
border-radius: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.items {
|
&__items {
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -63,29 +64,4 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 3rem;
|
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,
|
FC, memo, useLayoutEffect, useRef,
|
||||||
} from '../../../lib/teact/teact';
|
} 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 useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
|
||||||
import { getTouchY } from '../../../util/scrollLock';
|
import { getTouchY } from '../../../util/scrollLock';
|
||||||
|
import { createClassNameBuilder } from '../../../util/buildClassName';
|
||||||
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
|
import ReactionSelectorReaction from './ReactionSelectorReaction';
|
||||||
import AnimatedSticker from '../../common/AnimatedSticker';
|
|
||||||
|
|
||||||
import './ReactionSelector.scss';
|
import './ReactionSelector.scss';
|
||||||
|
|
||||||
const REACTION_SIZE = 32;
|
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
enabledReactions?: string[];
|
enabledReactions?: string[];
|
||||||
onSendReaction: (reaction: string, x: number, y: number) => void;
|
onSendReaction: (reaction: string, x: number, y: number) => void;
|
||||||
@ -25,57 +20,8 @@ type OwnProps = {
|
|||||||
isReady?: boolean;
|
isReady?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AvailableReaction: FC<{
|
const cn = createClassNameBuilder('ReactionSelector');
|
||||||
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 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> = ({
|
const ReactionSelector: FC<OwnProps> = ({
|
||||||
availableReactions,
|
availableReactions,
|
||||||
enabledReactions,
|
enabledReactions,
|
||||||
@ -104,16 +50,16 @@ const ReactionSelector: FC<OwnProps> = ({
|
|||||||
if ((!isPrivate && !enabledReactions?.length) || !availableReactions) return undefined;
|
if ((!isPrivate && !enabledReactions?.length) || !availableReactions) return undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ReactionSelector" onWheelCapture={handleWheel} onTouchMove={handleWheel}>
|
<div className={cn('&')} onWheelCapture={handleWheel} onTouchMove={handleWheel}>
|
||||||
<div className="bubble-big" />
|
<div className={cn('bubble-big')} />
|
||||||
<div className="bubble-small" />
|
<div className={cn('bubble-small')} />
|
||||||
<div className="items-wrapper">
|
<div className={cn('items-wrapper')}>
|
||||||
<div className="items no-scrollbar" ref={itemsScrollRef}>
|
<div className={cn('items', ['no-scrollbar'])} ref={itemsScrollRef}>
|
||||||
{availableReactions?.map((reaction) => {
|
{availableReactions?.map((reaction) => {
|
||||||
if (reaction.isInactive
|
if (reaction.isInactive
|
||||||
|| (!isPrivate && (!enabledReactions || !enabledReactions.includes(reaction.reaction)))) return undefined;
|
|| (!isPrivate && (!enabledReactions || !enabledReactions.includes(reaction.reaction)))) return undefined;
|
||||||
return (
|
return (
|
||||||
<AvailableReaction
|
<ReactionSelectorReaction
|
||||||
key={reaction.reaction}
|
key={reaction.reaction}
|
||||||
isReady={isReady}
|
isReady={isReady}
|
||||||
onSendReaction={onSendReaction}
|
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,17 +1,23 @@
|
|||||||
type Parts = (string | false | undefined)[];
|
type Parts = (string | false | undefined)[];
|
||||||
|
type PartsWithGlobals = (string | false | undefined | string[])[];
|
||||||
|
|
||||||
export default function buildClassName(...parts: Parts) {
|
export default function buildClassName(...parts: Parts) {
|
||||||
return parts.filter(Boolean).join(' ');
|
return parts.filter(Boolean).join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createClassNameBuilder(componentName: string) {
|
export function createClassNameBuilder(componentName: string) {
|
||||||
return (elementName: string, ...modifiers: Parts) => {
|
return (elementName: string, ...modifiers: PartsWithGlobals) => {
|
||||||
const baseName = elementName === '&' ? componentName : `${componentName}__${elementName}`;
|
const baseName = elementName === '&' ? componentName : `${componentName}__${elementName}`;
|
||||||
|
|
||||||
return modifiers.reduce((acc, modifier) => {
|
return modifiers.reduce<string[]>((acc, modifier) => {
|
||||||
if (modifier) {
|
if (modifier) {
|
||||||
|
// A bit hacky way to pass global class names
|
||||||
|
if (Array.isArray(modifier)) {
|
||||||
|
acc.push(...modifier);
|
||||||
|
} else {
|
||||||
acc.push(`${baseName}--${modifier}`);
|
acc.push(`${baseName}--${modifier}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, [baseName]).join(' ');
|
}, [baseName]).join(' ');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user