Reaction Picker: Display custom reactions in channels (#4069)

This commit is contained in:
Alexander Zinchuk 2023-12-12 12:34:31 +01:00
parent d32afadbd1
commit ee60271136
4 changed files with 125 additions and 24 deletions

View File

@ -15,13 +15,13 @@ import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import Button from '../../ui/Button';
import ReactionSelectorCustomReaction from './ReactionSelectorCustomReaction';
import ReactionSelectorReaction from './ReactionSelectorReaction';
import './ReactionSelector.scss';
type OwnProps = {
enabledReactions?: ApiChatReactions;
onToggleReaction: (reaction: ApiReaction) => void;
isPrivate?: boolean;
topReactions?: ApiReaction[];
allAvailableReactions?: ApiAvailableReaction[];
@ -31,12 +31,14 @@ type OwnProps = {
canBuyPremium?: boolean;
isCurrentUserPremium?: boolean;
canPlayAnimatedEmojis?: boolean;
onShowMore: (position: IAnchorPosition) => void;
className?: string;
onToggleReaction: (reaction: ApiReaction) => void;
onShowMore: (position: IAnchorPosition) => void;
};
const cn = createClassNameBuilder('ReactionSelector');
const REACTIONS_AMOUNT = 6;
const FADE_IN_DELAY = 20;
const ReactionSelector: FC<OwnProps> = ({
allAvailableReactions,
@ -47,31 +49,39 @@ const ReactionSelector: FC<OwnProps> = ({
isPrivate,
isReady,
canPlayAnimatedEmojis,
className,
onToggleReaction,
onShowMore,
className,
}) => {
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
const lang = useLang();
const availableReactions = useMemo(() => {
const reactions = allAvailableReactions?.map((availableReaction) => {
if (availableReaction.isInactive) return undefined;
if (!isPrivate && (!enabledReactions || !canSendReaction(availableReaction.reaction, enabledReactions))) {
const reactions = (enabledReactions?.type === 'some' && enabledReactions.allowed)
|| allAvailableReactions?.map((reaction) => reaction.reaction);
const filteredReactions = reactions?.map((reaction) => {
const isCustomReaction = 'documentId' in reaction;
const availableReaction = allAvailableReactions?.find((r) => isSameReaction(r.reaction, reaction));
if ((!isCustomReaction && !availableReaction) || availableReaction?.isInactive) return undefined;
if (!isPrivate && (!enabledReactions || !canSendReaction(reaction, enabledReactions))) {
return undefined;
}
if (maxUniqueReactions && currentReactions && currentReactions.length >= maxUniqueReactions
&& !currentReactions.some(({ reaction }) => isSameReaction(reaction, availableReaction.reaction))) {
&& !currentReactions.some(({ reaction: currentReaction }) => isSameReaction(reaction, currentReaction))) {
return undefined;
}
return availableReaction;
return isCustomReaction ? reaction : availableReaction;
}).filter(Boolean) || [];
return sortReactions(reactions, topReactions);
return sortReactions(filteredReactions, topReactions);
}, [allAvailableReactions, currentReactions, enabledReactions, isPrivate, maxUniqueReactions, topReactions]);
const reactionsToRender = useMemo(() => {
// Component can fit one more if we do not need show more button
return availableReactions.length === REACTIONS_AMOUNT + 1
? availableReactions
: availableReactions.slice(0, REACTIONS_AMOUNT);
@ -81,7 +91,7 @@ const ReactionSelector: FC<OwnProps> = ({
const userReactionIndexes = useMemo(() => {
const chosenReactions = currentReactions?.filter(({ chosenOrder }) => chosenOrder !== undefined) || [];
return new Set(chosenReactions.map(({ reaction }) => (
reactionsToRender.findIndex((r) => r && isSameReaction(r.reaction, reaction))
reactionsToRender.findIndex((r) => r && isSameReaction('reaction' in r ? r.reaction : r, reaction))
)));
}, [currentReactions, reactionsToRender]);
@ -102,14 +112,26 @@ const ReactionSelector: FC<OwnProps> = ({
<div className={cn('bubble-big', lang.isRtl && 'isRtl')} />
<div className={cn('items')} dir={lang.isRtl ? 'rtl' : undefined}>
{reactionsToRender.map((reaction, i) => (
<ReactionSelectorReaction
key={getReactionUniqueKey(reaction.reaction)}
isReady={isReady}
onToggleReaction={onToggleReaction}
reaction={reaction}
noAppearAnimation={!canPlayAnimatedEmojis}
chosen={userReactionIndexes.has(i)}
/>
'reaction' in reaction ? (
<ReactionSelectorReaction
key={getReactionUniqueKey(reaction.reaction)}
isReady={isReady}
onToggleReaction={onToggleReaction}
reaction={reaction}
noAppearAnimation={!canPlayAnimatedEmojis}
chosen={userReactionIndexes.has(i)}
/>
) : (
<ReactionSelectorCustomReaction
key={getReactionUniqueKey(reaction)}
isReady={isReady}
onToggleReaction={onToggleReaction}
reaction={reaction}
noAppearAnimation={!canPlayAnimatedEmojis}
chosen={userReactionIndexes.has(i)}
style={`--_animation-delay: ${(REACTIONS_AMOUNT - i) * FADE_IN_DELAY}ms`}
/>
)
))}
{withMoreButton && (
<Button

View File

@ -0,0 +1,58 @@
import type { FC } from '../../../lib/teact/teact';
import React, { memo } from '../../../lib/teact/teact';
import type { ApiReaction, ApiReactionCustomEmoji } from '../../../api/types';
import { createClassNameBuilder } from '../../../util/buildClassName';
import { REM } from '../../common/helpers/mediaDimensions';
import CustomEmoji from '../../common/CustomEmoji';
import './ReactionSelectorReaction.scss';
const REACTION_SIZE = 2 * REM;
type OwnProps = {
reaction: ApiReactionCustomEmoji;
chosen?: boolean;
isReady?: boolean;
noAppearAnimation?: boolean;
style?: string;
onToggleReaction: (reaction: ApiReaction) => void;
};
const cn = createClassNameBuilder('ReactionSelectorReaction');
const ReactionSelectorCustomReaction: FC<OwnProps> = ({
reaction,
chosen,
isReady,
noAppearAnimation,
style,
onToggleReaction,
}) => {
function handleClick() {
onToggleReaction(reaction);
}
return (
<div
className={cn(
'&',
'custom',
chosen && 'chosen',
!noAppearAnimation && isReady && 'custom-animated',
noAppearAnimation && 'visible',
)}
style={style}
onClick={handleClick}
>
<CustomEmoji
documentId={reaction.documentId}
size={REACTION_SIZE}
/>
</div>
);
};
export default memo(ReactionSelectorCustomReaction);

View File

@ -1,4 +1,6 @@
.ReactionSelectorReaction {
--custom-emoji-size: 2rem;
margin-inline-start: 0.25rem;
position: relative;
min-width: 2rem;
@ -22,11 +24,6 @@
left: 0;
}
&--compact {
min-width: 1.5rem;
min-height: 1.5rem;
}
&--chosen::before {
content: '';
position: absolute;
@ -38,4 +35,28 @@
border-radius: 50%;
background-color: var(--color-background-compact-menu-hover);
}
&--custom {
opacity: 0;
}
&--visible {
opacity: 1;
}
&--custom-animated {
animation: ReactionSelectorReaction--fade-in 0.2s ease-in-out forwards;
animation-delay: var(--_animation-delay);
}
@keyframes ReactionSelectorReaction--fade-in {
0% {
transform: scale(0.5);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}
}

View File

@ -54,7 +54,7 @@ const ReactionSelectorReaction: FC<OwnProps> = ({
<img
className={cn('static-icon')}
src={staticIconData}
alt=""
alt={reaction.reaction.emoticon}
draggable={false}
/>
)}