Reaction Picker: Display custom reactions in channels (#4069)
This commit is contained in:
parent
d32afadbd1
commit
ee60271136
@ -15,13 +15,13 @@ import useLang from '../../../hooks/useLang';
|
|||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
|
import ReactionSelectorCustomReaction from './ReactionSelectorCustomReaction';
|
||||||
import ReactionSelectorReaction from './ReactionSelectorReaction';
|
import ReactionSelectorReaction from './ReactionSelectorReaction';
|
||||||
|
|
||||||
import './ReactionSelector.scss';
|
import './ReactionSelector.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
enabledReactions?: ApiChatReactions;
|
enabledReactions?: ApiChatReactions;
|
||||||
onToggleReaction: (reaction: ApiReaction) => void;
|
|
||||||
isPrivate?: boolean;
|
isPrivate?: boolean;
|
||||||
topReactions?: ApiReaction[];
|
topReactions?: ApiReaction[];
|
||||||
allAvailableReactions?: ApiAvailableReaction[];
|
allAvailableReactions?: ApiAvailableReaction[];
|
||||||
@ -31,12 +31,14 @@ type OwnProps = {
|
|||||||
canBuyPremium?: boolean;
|
canBuyPremium?: boolean;
|
||||||
isCurrentUserPremium?: boolean;
|
isCurrentUserPremium?: boolean;
|
||||||
canPlayAnimatedEmojis?: boolean;
|
canPlayAnimatedEmojis?: boolean;
|
||||||
onShowMore: (position: IAnchorPosition) => void;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
|
onToggleReaction: (reaction: ApiReaction) => void;
|
||||||
|
onShowMore: (position: IAnchorPosition) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const cn = createClassNameBuilder('ReactionSelector');
|
const cn = createClassNameBuilder('ReactionSelector');
|
||||||
const REACTIONS_AMOUNT = 6;
|
const REACTIONS_AMOUNT = 6;
|
||||||
|
const FADE_IN_DELAY = 20;
|
||||||
|
|
||||||
const ReactionSelector: FC<OwnProps> = ({
|
const ReactionSelector: FC<OwnProps> = ({
|
||||||
allAvailableReactions,
|
allAvailableReactions,
|
||||||
@ -47,31 +49,39 @@ const ReactionSelector: FC<OwnProps> = ({
|
|||||||
isPrivate,
|
isPrivate,
|
||||||
isReady,
|
isReady,
|
||||||
canPlayAnimatedEmojis,
|
canPlayAnimatedEmojis,
|
||||||
|
className,
|
||||||
onToggleReaction,
|
onToggleReaction,
|
||||||
onShowMore,
|
onShowMore,
|
||||||
className,
|
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
const availableReactions = useMemo(() => {
|
const availableReactions = useMemo(() => {
|
||||||
const reactions = allAvailableReactions?.map((availableReaction) => {
|
const reactions = (enabledReactions?.type === 'some' && enabledReactions.allowed)
|
||||||
if (availableReaction.isInactive) return undefined;
|
|| allAvailableReactions?.map((reaction) => reaction.reaction);
|
||||||
if (!isPrivate && (!enabledReactions || !canSendReaction(availableReaction.reaction, enabledReactions))) {
|
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;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxUniqueReactions && currentReactions && currentReactions.length >= maxUniqueReactions
|
if (maxUniqueReactions && currentReactions && currentReactions.length >= maxUniqueReactions
|
||||||
&& !currentReactions.some(({ reaction }) => isSameReaction(reaction, availableReaction.reaction))) {
|
&& !currentReactions.some(({ reaction: currentReaction }) => isSameReaction(reaction, currentReaction))) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return availableReaction;
|
|
||||||
|
return isCustomReaction ? reaction : availableReaction;
|
||||||
}).filter(Boolean) || [];
|
}).filter(Boolean) || [];
|
||||||
|
|
||||||
return sortReactions(reactions, topReactions);
|
return sortReactions(filteredReactions, topReactions);
|
||||||
}, [allAvailableReactions, currentReactions, enabledReactions, isPrivate, maxUniqueReactions, topReactions]);
|
}, [allAvailableReactions, currentReactions, enabledReactions, isPrivate, maxUniqueReactions, topReactions]);
|
||||||
|
|
||||||
const reactionsToRender = useMemo(() => {
|
const reactionsToRender = useMemo(() => {
|
||||||
|
// Component can fit one more if we do not need show more button
|
||||||
return availableReactions.length === REACTIONS_AMOUNT + 1
|
return availableReactions.length === REACTIONS_AMOUNT + 1
|
||||||
? availableReactions
|
? availableReactions
|
||||||
: availableReactions.slice(0, REACTIONS_AMOUNT);
|
: availableReactions.slice(0, REACTIONS_AMOUNT);
|
||||||
@ -81,7 +91,7 @@ const ReactionSelector: FC<OwnProps> = ({
|
|||||||
const userReactionIndexes = useMemo(() => {
|
const userReactionIndexes = useMemo(() => {
|
||||||
const chosenReactions = currentReactions?.filter(({ chosenOrder }) => chosenOrder !== undefined) || [];
|
const chosenReactions = currentReactions?.filter(({ chosenOrder }) => chosenOrder !== undefined) || [];
|
||||||
return new Set(chosenReactions.map(({ reaction }) => (
|
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]);
|
}, [currentReactions, reactionsToRender]);
|
||||||
|
|
||||||
@ -102,14 +112,26 @@ const ReactionSelector: FC<OwnProps> = ({
|
|||||||
<div className={cn('bubble-big', lang.isRtl && 'isRtl')} />
|
<div className={cn('bubble-big', lang.isRtl && 'isRtl')} />
|
||||||
<div className={cn('items')} dir={lang.isRtl ? 'rtl' : undefined}>
|
<div className={cn('items')} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
{reactionsToRender.map((reaction, i) => (
|
{reactionsToRender.map((reaction, i) => (
|
||||||
<ReactionSelectorReaction
|
'reaction' in reaction ? (
|
||||||
key={getReactionUniqueKey(reaction.reaction)}
|
<ReactionSelectorReaction
|
||||||
isReady={isReady}
|
key={getReactionUniqueKey(reaction.reaction)}
|
||||||
onToggleReaction={onToggleReaction}
|
isReady={isReady}
|
||||||
reaction={reaction}
|
onToggleReaction={onToggleReaction}
|
||||||
noAppearAnimation={!canPlayAnimatedEmojis}
|
reaction={reaction}
|
||||||
chosen={userReactionIndexes.has(i)}
|
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 && (
|
{withMoreButton && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -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);
|
||||||
@ -1,4 +1,6 @@
|
|||||||
.ReactionSelectorReaction {
|
.ReactionSelectorReaction {
|
||||||
|
--custom-emoji-size: 2rem;
|
||||||
|
|
||||||
margin-inline-start: 0.25rem;
|
margin-inline-start: 0.25rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
min-width: 2rem;
|
min-width: 2rem;
|
||||||
@ -22,11 +24,6 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--compact {
|
|
||||||
min-width: 1.5rem;
|
|
||||||
min-height: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--chosen::before {
|
&--chosen::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -38,4 +35,28 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: var(--color-background-compact-menu-hover);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,7 +54,7 @@ const ReactionSelectorReaction: FC<OwnProps> = ({
|
|||||||
<img
|
<img
|
||||||
className={cn('static-icon')}
|
className={cn('static-icon')}
|
||||||
src={staticIconData}
|
src={staticIconData}
|
||||||
alt=""
|
alt={reaction.reaction.emoticon}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user