305 lines
10 KiB
TypeScript
305 lines
10 KiB
TypeScript
import type { FC } from '../../../../lib/teact/teact';
|
|
import React, { memo, useMemo, useRef } from '../../../../lib/teact/teact';
|
|
import { getActions, getGlobal, withGlobal } from '../../../../global';
|
|
|
|
import type { IAnchorPosition } from '../../../../types';
|
|
import {
|
|
type ApiAvailableEffect,
|
|
type ApiMessage, type ApiMessageEntity,
|
|
type ApiReaction, type ApiReactionCustomEmoji, type ApiSticker, type ApiStory, type ApiStorySkipped,
|
|
MAIN_THREAD_ID,
|
|
} from '../../../../api/types';
|
|
|
|
import { getReactionKey, getStoryKey, isUserId } from '../../../../global/helpers';
|
|
import {
|
|
selectChat, selectChatFullInfo, selectChatMessage, selectIsContextMenuTranslucent, selectIsCurrentUserPremium,
|
|
selectPeerStory, selectTabState,
|
|
} from '../../../../global/selectors';
|
|
import buildClassName from '../../../../util/buildClassName';
|
|
import parseHtmlAsFormattedText from '../../../../util/parseHtmlAsFormattedText';
|
|
import { REM } from '../../../common/helpers/mediaDimensions';
|
|
import { buildCustomEmojiHtml } from '../../composer/helpers/customEmoji';
|
|
|
|
import { getIsMobile } from '../../../../hooks/useAppLayout';
|
|
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
|
|
import useLastCallback from '../../../../hooks/useLastCallback';
|
|
import useMenuPosition from '../../../../hooks/useMenuPosition';
|
|
import useOldLang from '../../../../hooks/useOldLang';
|
|
|
|
import CustomEmojiPicker from '../../../common/CustomEmojiPicker';
|
|
import Menu from '../../../ui/Menu';
|
|
import StickerPicker from '../../composer/StickerPicker';
|
|
import ReactionPickerLimited from './ReactionPickerLimited';
|
|
|
|
import styles from './ReactionPicker.module.scss';
|
|
|
|
export type OwnProps = {
|
|
isOpen: boolean;
|
|
};
|
|
|
|
interface StateProps {
|
|
withCustomReactions?: boolean;
|
|
message?: ApiMessage;
|
|
story?: ApiStory | ApiStorySkipped;
|
|
isCurrentUserPremium?: boolean;
|
|
position?: IAnchorPosition;
|
|
isTranslucent?: boolean;
|
|
sendAsMessage?: boolean;
|
|
chatId?: string;
|
|
isForEffects?: boolean;
|
|
availableEffectById: Record<string, ApiAvailableEffect>;
|
|
}
|
|
|
|
const FULL_PICKER_SHIFT_DELTA = { x: -23, y: -64 };
|
|
const LIMITED_PICKER_SHIFT_DELTA = { x: -21, y: -10 };
|
|
const REACTION_SELECTOR_WIDTH = 16.375 * REM;
|
|
|
|
const ReactionPicker: FC<OwnProps & StateProps> = ({
|
|
isOpen,
|
|
message,
|
|
story,
|
|
position,
|
|
isTranslucent,
|
|
isCurrentUserPremium,
|
|
withCustomReactions,
|
|
sendAsMessage,
|
|
chatId,
|
|
isForEffects,
|
|
availableEffectById,
|
|
}) => {
|
|
const {
|
|
toggleReaction, closeReactionPicker, sendMessage, showNotification, sendStoryReaction, saveEffectInDraft,
|
|
requestEffectInComposer,
|
|
} = getActions();
|
|
|
|
const lang = useOldLang();
|
|
|
|
const renderedMessageId = useCurrentOrPrev(message?.id, true);
|
|
const renderedChatId = useCurrentOrPrev(message?.chatId, true);
|
|
const renderedStoryPeerId = useCurrentOrPrev(story?.peerId, true);
|
|
const renderedStoryId = useCurrentOrPrev(story?.id);
|
|
const storedPosition = useCurrentOrPrev(position, true);
|
|
// eslint-disable-next-line no-null/no-null
|
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
const renderingPosition = useMemo((): IAnchorPosition | undefined => {
|
|
if (!storedPosition) {
|
|
return undefined;
|
|
}
|
|
|
|
if (renderedStoryId) {
|
|
return storedPosition;
|
|
}
|
|
|
|
return {
|
|
x: storedPosition.x + (withCustomReactions ? FULL_PICKER_SHIFT_DELTA.x : LIMITED_PICKER_SHIFT_DELTA.x),
|
|
y: storedPosition.y + (withCustomReactions ? FULL_PICKER_SHIFT_DELTA.y : LIMITED_PICKER_SHIFT_DELTA.y),
|
|
};
|
|
}, [renderedStoryId, storedPosition, withCustomReactions]);
|
|
|
|
const getMenuElement = useLastCallback(() => menuRef.current);
|
|
const getLayout = useLastCallback(() => ({
|
|
withPortal: true,
|
|
isDense: !renderedStoryPeerId,
|
|
deltaX: !getIsMobile() && menuRef.current
|
|
? -(menuRef.current.offsetWidth - REACTION_SELECTOR_WIDTH) / 2 - FULL_PICKER_SHIFT_DELTA.x / 2
|
|
: 0,
|
|
}));
|
|
const {
|
|
positionX, positionY, transformOriginX, transformOriginY, style,
|
|
} = useMenuPosition(renderingPosition, getTriggerElement, getRootElement, getMenuElement, getLayout);
|
|
|
|
const handleToggleCustomReaction = useLastCallback((sticker: ApiSticker) => {
|
|
if (!renderedChatId || !renderedMessageId) {
|
|
return;
|
|
}
|
|
const reaction = sticker.isCustomEmoji
|
|
? { documentId: sticker.id } as ApiReactionCustomEmoji
|
|
: { emoticon: sticker.emoji } as ApiReaction;
|
|
|
|
toggleReaction({
|
|
chatId: renderedChatId, messageId: renderedMessageId, reaction, shouldAddToRecent: true,
|
|
});
|
|
closeReactionPicker();
|
|
});
|
|
|
|
const handleToggleReaction = useLastCallback((reaction: ApiReaction) => {
|
|
if (!renderedChatId || !renderedMessageId) {
|
|
return;
|
|
}
|
|
|
|
toggleReaction({
|
|
chatId: renderedChatId, messageId: renderedMessageId, reaction, shouldAddToRecent: true,
|
|
});
|
|
closeReactionPicker();
|
|
});
|
|
|
|
const handleStoryReactionSelect = useLastCallback((item: ApiReaction | ApiSticker) => {
|
|
const reaction = 'id' in item ? { documentId: item.id } : item;
|
|
|
|
const sticker = 'documentId' in item
|
|
? getGlobal().customEmojis.byId[item.documentId] : 'emoticon' in item ? undefined : item;
|
|
|
|
if (sticker && !sticker.isFree && !isCurrentUserPremium) {
|
|
showNotification({
|
|
message: lang('UnlockPremiumEmojiHint'),
|
|
action: {
|
|
action: 'openPremiumModal',
|
|
payload: { initialSection: 'animated_emoji' },
|
|
},
|
|
actionText: lang('PremiumMore'),
|
|
});
|
|
|
|
closeReactionPicker();
|
|
|
|
return;
|
|
}
|
|
|
|
if (!sendAsMessage) {
|
|
sendStoryReaction({
|
|
peerId: renderedStoryPeerId!,
|
|
storyId: renderedStoryId!,
|
|
containerId: getStoryKey(renderedStoryPeerId!, renderedStoryId!),
|
|
reaction,
|
|
shouldAddToRecent: true,
|
|
});
|
|
closeReactionPicker();
|
|
return;
|
|
}
|
|
|
|
let text: string | undefined;
|
|
let entities: ApiMessageEntity[] | undefined;
|
|
|
|
if ('emoticon' in item) {
|
|
text = item.emoticon;
|
|
} else {
|
|
const customEmojiMessage = parseHtmlAsFormattedText(buildCustomEmojiHtml(sticker!));
|
|
text = customEmojiMessage.text;
|
|
entities = customEmojiMessage.entities;
|
|
}
|
|
|
|
sendMessage({ text, entities, isReaction: true });
|
|
closeReactionPicker();
|
|
});
|
|
|
|
const handleStickerSelect = useLastCallback((sticker: ApiSticker) => {
|
|
const availableEffects = Object.values(availableEffectById);
|
|
const effectId = availableEffects.find((effect) => effect.effectStickerId === sticker.id)?.id;
|
|
|
|
if (chatId) saveEffectInDraft({ chatId, threadId: MAIN_THREAD_ID, effectId });
|
|
|
|
if (effectId) requestEffectInComposer({ });
|
|
closeReactionPicker();
|
|
});
|
|
|
|
const selectedReactionIds = useMemo(() => {
|
|
return (message?.reactions?.results || []).reduce<string[]>((acc, { chosenOrder, reaction }) => {
|
|
if (chosenOrder !== undefined) {
|
|
acc.push(getReactionKey(reaction));
|
|
}
|
|
|
|
return acc;
|
|
}, []);
|
|
}, [message?.reactions?.results]);
|
|
|
|
return (
|
|
<Menu
|
|
isOpen={isOpen}
|
|
ref={menuRef}
|
|
className={buildClassName(styles.menu, 'ReactionPicker')}
|
|
bubbleClassName={buildClassName(
|
|
styles.menuContent,
|
|
!withCustomReactions && !renderedStoryId && styles.onlyReactions,
|
|
renderedStoryId && styles.storyMenu,
|
|
)}
|
|
withPortal
|
|
noCompact
|
|
positionX={positionX}
|
|
positionY={positionY}
|
|
transformOriginX={transformOriginX}
|
|
transformOriginY={transformOriginY}
|
|
style={style}
|
|
backdropExcludedSelector=".Modal.confirm"
|
|
onClose={closeReactionPicker}
|
|
>
|
|
{isForEffects && chatId ? (
|
|
<StickerPicker
|
|
className=""
|
|
isHidden={!isOpen}
|
|
loadAndPlay={Boolean(isOpen && withCustomReactions)}
|
|
idPrefix="message-effect"
|
|
canSendStickers={false}
|
|
noContextMenus={false}
|
|
chatId={chatId}
|
|
isTranslucent={isTranslucent}
|
|
onStickerSelect={handleStickerSelect}
|
|
isForEffects={isForEffects}
|
|
/>
|
|
) : (
|
|
<>
|
|
<CustomEmojiPicker
|
|
chatId={renderedChatId}
|
|
idPrefix="message-emoji-set-"
|
|
isHidden={!isOpen || !(withCustomReactions || renderedStoryId)}
|
|
loadAndPlay={Boolean(isOpen && withCustomReactions)}
|
|
isReactionPicker
|
|
className={!withCustomReactions && !renderedStoryId ? styles.hidden : undefined}
|
|
selectedReactionIds={selectedReactionIds}
|
|
isTranslucent={isTranslucent}
|
|
onCustomEmojiSelect={renderedStoryId ? handleStoryReactionSelect : handleToggleCustomReaction}
|
|
onReactionSelect={renderedStoryId ? handleStoryReactionSelect : handleToggleReaction}
|
|
/>
|
|
{!withCustomReactions && Boolean(renderedChatId) && (
|
|
<ReactionPickerLimited
|
|
chatId={renderedChatId}
|
|
loadAndPlay={isOpen}
|
|
onReactionSelect={renderedStoryId ? handleStoryReactionSelect : handleToggleReaction}
|
|
selectedReactionIds={selectedReactionIds}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</Menu>
|
|
);
|
|
};
|
|
|
|
export default memo(withGlobal<OwnProps>((global): StateProps => {
|
|
const state = selectTabState(global);
|
|
const availableEffectById = global.availableEffectById;
|
|
const {
|
|
chatId, messageId, storyPeerId, storyId, position, sendAsMessage, isForEffects,
|
|
} = state.reactionPicker || {};
|
|
const story = storyPeerId && storyId
|
|
? selectPeerStory(global, storyPeerId, storyId) as ApiStory | ApiStorySkipped
|
|
: undefined;
|
|
const chat = chatId ? selectChat(global, chatId) : undefined;
|
|
const chatFullInfo = chatId ? selectChatFullInfo(global, chatId) : undefined;
|
|
const message = chatId && messageId ? selectChatMessage(global, chatId, messageId) : undefined;
|
|
const isPrivateChat = isUserId(chatId || storyPeerId || '');
|
|
const areSomeReactionsAllowed = chatFullInfo?.enabledReactions?.type === 'some';
|
|
const areCustomReactionsAllowed = chatFullInfo?.enabledReactions?.type === 'all'
|
|
&& chatFullInfo?.enabledReactions?.areCustomAllowed;
|
|
|
|
return {
|
|
message,
|
|
story,
|
|
position,
|
|
withCustomReactions: chat?.isForbidden || areSomeReactionsAllowed
|
|
? false
|
|
: areCustomReactionsAllowed || isPrivateChat,
|
|
isTranslucent: selectIsContextMenuTranslucent(global),
|
|
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
|
sendAsMessage,
|
|
isForEffects,
|
|
chatId,
|
|
availableEffectById,
|
|
};
|
|
})(ReactionPicker));
|
|
|
|
function getTriggerElement(): HTMLElement | null {
|
|
return document.querySelector('body');
|
|
}
|
|
|
|
function getRootElement() {
|
|
return document.querySelector('body');
|
|
}
|