TelegramPWA/src/components/middle/message/MessageContextMenu.tsx

280 lines
9.1 KiB
TypeScript

import React, {
FC, memo, useCallback, useEffect, useRef,
} from '../../../lib/teact/teact';
import { ApiAvailableReaction, ApiMessage, ApiUser } from '../../../api/types';
import { IAnchorPosition } from '../../../types';
import { getMessageCopyOptions } from './helpers/copyOptions';
import { disableScrolling, enableScrolling } from '../../../util/scrollLock';
import useContextMenuPosition from '../../../hooks/useContextMenuPosition';
import useLang from '../../../hooks/useLang';
import buildClassName from '../../../util/buildClassName';
import useFlag from '../../../hooks/useFlag';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import Menu from '../../ui/Menu';
import MenuItem from '../../ui/MenuItem';
import Avatar from '../../common/Avatar';
import ReactionSelector from './ReactionSelector';
import './MessageContextMenu.scss';
type OwnProps = {
availableReactions?: ApiAvailableReaction[];
isOpen: boolean;
anchor: IAnchorPosition;
message: ApiMessage;
canSendNow?: boolean;
enabledReactions?: string[];
canReschedule?: boolean;
canReply?: boolean;
canPin?: boolean;
canUnpin?: boolean;
canDelete?: boolean;
canReport?: boolean;
canShowReactionsCount?: boolean;
canShowReactionList?: boolean;
canRemoveReaction?: boolean;
canEdit?: boolean;
canForward?: boolean;
canFaveSticker?: boolean;
canUnfaveSticker?: boolean;
canCopy?: boolean;
canCopyLink?: boolean;
canSelect?: boolean;
isPrivate?: boolean;
canDownload?: boolean;
isDownloading?: boolean;
canShowSeenBy?: boolean;
seenByRecentUsers?: ApiUser[];
onReply: () => void;
onEdit: () => void;
onPin: () => void;
onUnpin: () => void;
onForward: () => void;
onDelete: () => void;
onReport: () => void;
onFaveSticker: () => void;
onUnfaveSticker: () => void;
onSelect: () => void;
onSend: () => void;
onReschedule: () => void;
onClose: () => void;
onCloseAnimationEnd?: () => void;
onCopyLink?: () => void;
onDownload?: () => void;
onShowSeenBy?: () => void;
onShowReactors?: () => void;
onSendReaction: (reaction: string | undefined, x: number, y: number) => void;
};
const SCROLLBAR_WIDTH = 10;
const REACTION_BUBBLE_EXTRA_WIDTH = 32;
const ANIMATION_DURATION = 200;
const MessageContextMenu: FC<OwnProps> = ({
availableReactions,
isOpen,
message,
isPrivate,
enabledReactions,
anchor,
canSendNow,
canReschedule,
canReply,
canEdit,
canPin,
canUnpin,
canDelete,
canReport,
canForward,
canFaveSticker,
canUnfaveSticker,
canCopy,
canCopyLink,
canSelect,
canDownload,
isDownloading,
canShowSeenBy,
canShowReactionsCount,
canRemoveReaction,
canShowReactionList,
seenByRecentUsers,
onReply,
onEdit,
onPin,
onUnpin,
onForward,
onDelete,
onReport,
onFaveSticker,
onUnfaveSticker,
onSelect,
onSend,
onReschedule,
onClose,
onCloseAnimationEnd,
onCopyLink,
onDownload,
onShowSeenBy,
onShowReactors,
onSendReaction,
}) => {
// eslint-disable-next-line no-null/no-null
const menuRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const scrollableRef = useRef<HTMLDivElement>(null);
const copyOptions = getMessageCopyOptions(message, onClose, canCopyLink ? onCopyLink : undefined);
const noReactions = !isPrivate && !enabledReactions?.length;
const withReactions = canShowReactionList && !noReactions;
const [isReady, markIsReady, unmarkIsReady] = useFlag();
const getTriggerElement = useCallback(() => {
return document.querySelector(`.Transition__slide--active > .MessageList div[data-message-id="${message.id}"]`);
}, [message.id]);
const getRootElement = useCallback(
() => document.querySelector('.Transition__slide--active > .MessageList'),
[],
);
const getMenuElement = useCallback(
() => document.querySelector('.MessageContextMenu .bubble'),
[],
);
const getLayout = useCallback(() => {
const extraHeightAudioPlayer = (IS_SINGLE_COLUMN_LAYOUT
&& (document.querySelector<HTMLElement>('.AudioPlayer-content'))?.offsetHeight) || 0;
const pinnedElement = document.querySelector<HTMLElement>('.HeaderPinnedMessage-wrapper');
const extraHeightPinned = (((IS_SINGLE_COLUMN_LAYOUT && !extraHeightAudioPlayer)
|| (!IS_SINGLE_COLUMN_LAYOUT && pinnedElement?.classList.contains('full-width')))
&& pinnedElement?.offsetHeight) || 0;
return {
extraPaddingX: SCROLLBAR_WIDTH,
extraTopPadding: (document.querySelector<HTMLElement>('.MiddleHeader')!).offsetHeight,
marginSides: withReactions ? REACTION_BUBBLE_EXTRA_WIDTH : undefined,
extraMarginTop: extraHeightPinned + extraHeightAudioPlayer,
};
}, [withReactions]);
const handleRemoveReaction = useCallback(() => {
onSendReaction(undefined, 0, 0);
}, [onSendReaction]);
useEffect(() => {
if (!isOpen) {
unmarkIsReady();
return;
}
setTimeout(() => {
markIsReady();
}, ANIMATION_DURATION);
}, [isOpen, markIsReady, unmarkIsReady]);
const {
positionX, positionY, transformOriginX, transformOriginY, style, menuStyle, withScroll,
} = useContextMenuPosition(anchor, getTriggerElement, getRootElement, getMenuElement, getLayout);
useEffect(() => {
disableScrolling(withScroll ? scrollableRef.current : undefined, '.ReactionSelector');
return enableScrolling;
}, [withScroll]);
const lang = useLang();
return (
<Menu
ref={menuRef}
isOpen={isOpen}
transformOriginX={transformOriginX}
transformOriginY={transformOriginY}
positionX={positionX}
positionY={positionY}
style={style}
bubbleStyle={menuStyle}
className={buildClassName(
'MessageContextMenu', 'fluid', withReactions && 'with-reactions',
)}
onClose={onClose}
onCloseAnimationEnd={onCloseAnimationEnd}
>
{canShowReactionList && (
<ReactionSelector
enabledReactions={enabledReactions}
onSendReaction={onSendReaction}
isPrivate={isPrivate}
availableReactions={availableReactions}
isReady={isReady}
/>
)}
<div
className="scrollable-content custom-scroll"
style={menuStyle}
ref={scrollableRef}
>
{canRemoveReaction && <MenuItem icon="reactions" onClick={handleRemoveReaction}>Remove Reaction</MenuItem>}
{canSendNow && <MenuItem icon="send-outline" onClick={onSend}>{lang('MessageScheduleSend')}</MenuItem>}
{canReschedule && (
<MenuItem icon="schedule" onClick={onReschedule}>{lang('MessageScheduleEditTime')}</MenuItem>
)}
{canReply && <MenuItem icon="reply" onClick={onReply}>{lang('Reply')}</MenuItem>}
{canEdit && <MenuItem icon="edit" onClick={onEdit}>{lang('Edit')}</MenuItem>}
{canFaveSticker && (
<MenuItem icon="favorite" onClick={onFaveSticker}>{lang('AddToFavorites')}</MenuItem>
)}
{canUnfaveSticker && (
<MenuItem icon="favorite" onClick={onUnfaveSticker}>{lang('Stickers.RemoveFromFavorites')}</MenuItem>
)}
{canCopy && copyOptions.map((options) => (
<MenuItem key={options.label} icon="copy" onClick={options.handler}>{lang(options.label)}</MenuItem>
))}
{canPin && <MenuItem icon="pin" onClick={onPin}>{lang('DialogPin')}</MenuItem>}
{canUnpin && <MenuItem icon="unpin" onClick={onUnpin}>{lang('DialogUnpin')}</MenuItem>}
{canDownload && (
<MenuItem icon="download" onClick={onDownload}>
{isDownloading ? lang('lng_context_cancel_download') : lang('lng_media_download')}
</MenuItem>
)}
{canForward && <MenuItem icon="forward" onClick={onForward}>{lang('Forward')}</MenuItem>}
{canSelect && <MenuItem icon="select" onClick={onSelect}>{lang('Common.Select')}</MenuItem>}
{canReport && <MenuItem icon="flag" onClick={onReport}>{lang('lng_context_report_msg')}</MenuItem>}
{(canShowSeenBy || canShowReactionsCount) && (
<MenuItem
icon={canShowReactionsCount ? 'reactions' : 'group'}
onClick={canShowReactionsCount ? onShowReactors : onShowSeenBy}
disabled={!canShowReactionsCount && !message.seenByUserIds?.length}
>
{canShowReactionsCount && message.reactors?.count ? (
canShowSeenBy && message.seenByUserIds?.length
? lang('Chat.OutgoingContextMixedReactionCount', [message.reactors.count, message.seenByUserIds.length])
: lang('Chat.ContextReactionCount', message.reactors.count, 'i')
) : (
message.seenByUserIds?.length
? lang('Conversation.ContextMenuSeen', message.seenByUserIds.length, 'i')
: lang('Conversation.ContextMenuNoViews')
)}
<div className="avatars">
{seenByRecentUsers?.map((user) => (
<Avatar
size="micro"
user={user}
/>
))}
</div>
</MenuItem>
)}
{canDelete && <MenuItem destructive icon="delete" onClick={onDelete}>{lang('Delete')}</MenuItem>}
</div>
</Menu>
);
};
export default memo(MessageContextMenu);