249 lines
6.9 KiB
TypeScript
249 lines
6.9 KiB
TypeScript
import type { FC } from '../../../../lib/teact/teact';
|
|
import React, { memo, useEffect, useMemo } from '../../../../lib/teact/teact';
|
|
import { getActions, getGlobal } from '../../../../global';
|
|
|
|
import type {
|
|
ApiMessage,
|
|
ApiPeer,
|
|
ApiReaction,
|
|
ApiReactionKey,
|
|
ApiSavedReactionTag,
|
|
} from '../../../../api/types';
|
|
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
|
import type { ThreadId } from '../../../../types';
|
|
|
|
import { getReactionKey, isReactionChosen } from '../../../../global/helpers';
|
|
import { selectPeer } from '../../../../global/selectors';
|
|
import buildClassName from '../../../../util/buildClassName';
|
|
import { getMessageKey } from '../../../../util/keys/messageKey';
|
|
|
|
import useEffectOnce from '../../../../hooks/useEffectOnce';
|
|
import useLastCallback from '../../../../hooks/useLastCallback';
|
|
import useOldLang from '../../../../hooks/useOldLang';
|
|
|
|
import ReactionButton from './ReactionButton';
|
|
import SavedTagButton from './SavedTagButton';
|
|
|
|
import './Reactions.scss';
|
|
|
|
type OwnProps = {
|
|
message: ApiMessage;
|
|
threadId?: ThreadId;
|
|
isOutside?: boolean;
|
|
maxWidth?: number;
|
|
metaChildren?: React.ReactNode;
|
|
tags?: Record<ApiReactionKey, ApiSavedReactionTag>;
|
|
isCurrentUserPremium?: boolean;
|
|
observeIntersection?: ObserveFn;
|
|
noRecentReactors?: boolean;
|
|
isAccountFrozen?: boolean;
|
|
};
|
|
|
|
const MAX_RECENT_AVATARS = 3;
|
|
const PAID_SEND_DELAY = 5000;
|
|
|
|
const Reactions: FC<OwnProps> = ({
|
|
message,
|
|
threadId,
|
|
isOutside,
|
|
maxWidth,
|
|
metaChildren,
|
|
observeIntersection,
|
|
noRecentReactors,
|
|
isCurrentUserPremium,
|
|
tags,
|
|
isAccountFrozen,
|
|
}) => {
|
|
const {
|
|
toggleReaction,
|
|
addLocalPaidReaction,
|
|
updateMiddleSearch,
|
|
performMiddleSearch,
|
|
openPremiumModal,
|
|
resetLocalPaidReactions,
|
|
showNotification,
|
|
openFrozenAccountModal,
|
|
} = getActions();
|
|
const lang = useOldLang();
|
|
|
|
const { results, areTags, recentReactions } = message.reactions!;
|
|
const withServiceReactions = Boolean(message.areReactionsPossible && message.reactions);
|
|
|
|
const totalCount = useMemo(() => (
|
|
results.reduce((acc, reaction) => acc + reaction.count, 0)
|
|
), [results]);
|
|
|
|
const recentReactorsByReactionKey = useMemo(() => {
|
|
const global = getGlobal();
|
|
|
|
return recentReactions?.reduce((acc, recentReaction) => {
|
|
const { reaction, peerId } = recentReaction;
|
|
const key = getReactionKey(reaction);
|
|
const peer = selectPeer(global, peerId);
|
|
|
|
if (!peer) return acc;
|
|
|
|
const peers = acc[key] || [];
|
|
peers.push(peer);
|
|
acc[key] = peers;
|
|
return acc;
|
|
}, {} as Record<ApiReactionKey, ApiPeer[]>);
|
|
}, [recentReactions]);
|
|
|
|
const props = useMemo(() => {
|
|
const messageKey = getMessageKey(message);
|
|
return results.map((reaction) => {
|
|
const reactionKey = getReactionKey(reaction.reaction);
|
|
const recentReactors = recentReactorsByReactionKey?.[reactionKey];
|
|
const shouldHideRecentReactors = totalCount > MAX_RECENT_AVATARS || noRecentReactors;
|
|
const tag = areTags ? tags?.[reactionKey] : undefined;
|
|
|
|
return {
|
|
reaction,
|
|
reactionKey,
|
|
messageKey,
|
|
recentReactors: !shouldHideRecentReactors ? recentReactors : undefined,
|
|
isChosen: isReactionChosen(reaction),
|
|
tag,
|
|
};
|
|
});
|
|
}, [message, noRecentReactors, recentReactorsByReactionKey, results, areTags, tags, totalCount]);
|
|
|
|
const handleClick = useLastCallback((reaction: ApiReaction) => {
|
|
if (isAccountFrozen) {
|
|
openFrozenAccountModal();
|
|
return;
|
|
}
|
|
if (areTags) {
|
|
if (!isCurrentUserPremium) {
|
|
openPremiumModal({
|
|
initialSection: 'saved_tags',
|
|
});
|
|
return;
|
|
}
|
|
|
|
updateMiddleSearch({ chatId: message.chatId, threadId, update: { savedTag: reaction } });
|
|
performMiddleSearch({ chatId: message.chatId, threadId });
|
|
return;
|
|
}
|
|
|
|
toggleReaction({
|
|
chatId: message.chatId,
|
|
messageId: message.id,
|
|
reaction,
|
|
});
|
|
});
|
|
|
|
const paidLocalCount = useMemo(() => results.find((r) => r.reaction.type === 'paid')?.localAmount || 0, [results]);
|
|
|
|
const handlePaidClick = useLastCallback((count: number) => {
|
|
if (isAccountFrozen) {
|
|
openFrozenAccountModal();
|
|
return;
|
|
}
|
|
|
|
addLocalPaidReaction({
|
|
chatId: message.chatId,
|
|
messageId: message.id,
|
|
count,
|
|
});
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!paidLocalCount) return;
|
|
|
|
showNotification({
|
|
localId: getMessageKey(message),
|
|
title: lang('StarsSentTitle'),
|
|
message: lang('StarsSentText', paidLocalCount),
|
|
actionText: lang('StarsSentUndo'),
|
|
cacheBreaker: paidLocalCount.toString(),
|
|
action: {
|
|
action: 'resetLocalPaidReactions',
|
|
payload: { chatId: message.chatId, messageId: message.id },
|
|
},
|
|
dismissAction: {
|
|
action: 'sendPaidReaction',
|
|
payload: { chatId: message.chatId, messageId: message.id },
|
|
},
|
|
duration: PAID_SEND_DELAY,
|
|
shouldShowTimer: true,
|
|
disableClickDismiss: true,
|
|
icon: 'star',
|
|
});
|
|
}, [lang, message, paidLocalCount]);
|
|
|
|
const handleRemoveReaction = useLastCallback((reaction: ApiReaction) => {
|
|
if (isAccountFrozen) {
|
|
openFrozenAccountModal();
|
|
return;
|
|
}
|
|
|
|
toggleReaction({
|
|
chatId: message.chatId,
|
|
messageId: message.id,
|
|
reaction,
|
|
});
|
|
});
|
|
|
|
// Reset paid reactions on unmount
|
|
useEffectOnce(() => () => {
|
|
resetLocalPaidReactions({
|
|
chatId: message.chatId,
|
|
messageId: message.id,
|
|
});
|
|
});
|
|
|
|
return (
|
|
<div
|
|
className={buildClassName(
|
|
'Reactions',
|
|
isOutside && 'is-outside',
|
|
withServiceReactions && 'is-service',
|
|
)}
|
|
style={maxWidth ? `max-width: ${maxWidth}px` : undefined}
|
|
dir={lang.isRtl ? 'rtl' : 'ltr'}
|
|
>
|
|
{props.map(({
|
|
reaction, recentReactors, messageKey, reactionKey, isChosen, tag,
|
|
}) => (
|
|
areTags ? (
|
|
<SavedTagButton
|
|
key={reactionKey}
|
|
className="message-reaction"
|
|
chosenClassName="chosen"
|
|
containerId={messageKey}
|
|
isOwnMessage={message.isOutgoing}
|
|
isChosen={isChosen}
|
|
reaction={reaction.reaction as ApiReaction}
|
|
tag={tag}
|
|
withContextMenu={isCurrentUserPremium}
|
|
onClick={handleClick}
|
|
onRemove={handleRemoveReaction}
|
|
observeIntersection={observeIntersection}
|
|
/>
|
|
) : (
|
|
<ReactionButton
|
|
key={reactionKey}
|
|
chatId={message.chatId}
|
|
messageId={message.id}
|
|
className="message-reaction"
|
|
chosenClassName="chosen"
|
|
containerId={messageKey}
|
|
isOwnMessage={message.isOutgoing}
|
|
recentReactors={recentReactors}
|
|
isOutside={isOutside}
|
|
reaction={reaction}
|
|
onClick={handleClick}
|
|
onPaidClick={handlePaidClick}
|
|
observeIntersection={observeIntersection}
|
|
/>
|
|
)
|
|
))}
|
|
{metaChildren}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default memo(Reactions);
|