986 lines
30 KiB
TypeScript
986 lines
30 KiB
TypeScript
import type { FC } from '../../../lib/teact/teact';
|
|
import {
|
|
memo, useEffect, useMemo, useState,
|
|
} from '../../../lib/teact/teact';
|
|
import { getActions, getGlobal, withGlobal } from '../../../global';
|
|
|
|
import type {
|
|
ApiAvailableReaction,
|
|
ApiChat,
|
|
ApiChatReactions,
|
|
ApiMessage,
|
|
ApiPoll,
|
|
ApiReaction,
|
|
ApiStickerSet,
|
|
ApiStickerSetInfo,
|
|
ApiThreadInfo,
|
|
ApiTypeStory,
|
|
ApiWebPage,
|
|
} from '../../../api/types';
|
|
import type {
|
|
ActiveDownloads,
|
|
IAlbum,
|
|
IAnchorPosition,
|
|
MessageListType,
|
|
ThreadId,
|
|
} from '../../../types';
|
|
import { MAIN_THREAD_ID } from '../../../api/types';
|
|
|
|
import { PREVIEW_AVATAR_COUNT } from '../../../config';
|
|
import {
|
|
areReactionsEmpty,
|
|
getCanPostInChat,
|
|
getIsDownloading,
|
|
getMessageVideo,
|
|
getUserFullName,
|
|
hasMessageTtl,
|
|
isActionMessage,
|
|
isChatChannel,
|
|
isChatGroup,
|
|
isMessageLocal,
|
|
isOwnMessage,
|
|
isUserRightBanned,
|
|
} from '../../../global/helpers';
|
|
import {
|
|
selectActiveDownloads,
|
|
selectAllowedMessageActionsSlow,
|
|
selectBot,
|
|
selectCanForwardMessage,
|
|
selectCanGift,
|
|
selectCanPlayAnimatedEmojis,
|
|
selectCanScheduleUntilOnline,
|
|
selectCanTranslateMessage,
|
|
selectChat,
|
|
selectChatFullInfo,
|
|
selectCurrentMessageList,
|
|
selectIsChatWithSelf,
|
|
selectIsCurrentUserPremium,
|
|
selectIsMessageProtected,
|
|
selectIsMessageUnread,
|
|
selectIsPremiumPurchaseBlocked,
|
|
selectIsReactionPickerOpen,
|
|
selectMessageCustomEmojiSets,
|
|
selectMessageTranslations,
|
|
selectPeerStory,
|
|
selectPollFromMessage,
|
|
selectRequestedChatTranslationLanguage,
|
|
selectRequestedMessageTranslationLanguage,
|
|
selectStickerSet,
|
|
selectTopic,
|
|
selectUser,
|
|
selectUserFullInfo,
|
|
selectUserStatus,
|
|
selectWebPageFromMessage,
|
|
} from '../../../global/selectors';
|
|
import { selectMessageDownloadableMedia } from '../../../global/selectors/media';
|
|
import { selectSavedDialogIdFromMessage, selectThreadInfo } from '../../../global/selectors/threads';
|
|
import buildClassName from '../../../util/buildClassName';
|
|
import { copyTextToClipboard } from '../../../util/clipboard';
|
|
import { isUserId } from '../../../util/entities/ids';
|
|
import { getSelectionAsFormattedText } from './helpers/getSelectionAsFormattedText';
|
|
import { isSelectionRangeInsideMessage } from './helpers/isSelectionRangeInsideMessage';
|
|
|
|
import useFlag from '../../../hooks/useFlag';
|
|
import useLang from '../../../hooks/useLang';
|
|
import useLastCallback from '../../../hooks/useLastCallback';
|
|
import useOldLang from '../../../hooks/useOldLang';
|
|
import useSchedule from '../../../hooks/useSchedule';
|
|
import useShowTransition from '../../../hooks/useShowTransition';
|
|
|
|
import PinMessageModal from '../../common/PinMessageModal.async';
|
|
import ConfirmDialog from '../../ui/ConfirmDialog';
|
|
import MessageContextMenu from './MessageContextMenu';
|
|
|
|
export type OwnProps = {
|
|
isOpen: boolean;
|
|
message: ApiMessage;
|
|
album?: IAlbum;
|
|
anchor: IAnchorPosition;
|
|
targetHref?: string;
|
|
messageListType: MessageListType;
|
|
noReplies?: boolean;
|
|
detectedLanguage?: string;
|
|
repliesThreadInfo?: ApiThreadInfo;
|
|
className?: string;
|
|
onClose: NoneToVoidFunction;
|
|
onCloseAnimationEnd: NoneToVoidFunction;
|
|
};
|
|
|
|
type StateProps = {
|
|
threadId?: ThreadId;
|
|
poll?: ApiPoll;
|
|
webPage?: ApiWebPage;
|
|
story?: ApiTypeStory;
|
|
chat?: ApiChat;
|
|
availableReactions?: ApiAvailableReaction[];
|
|
topReactions?: ApiReaction[];
|
|
defaultTagReactions?: ApiReaction[];
|
|
noOptions?: boolean;
|
|
canSendNow?: boolean;
|
|
canReschedule?: boolean;
|
|
canReply?: boolean;
|
|
canPin?: boolean;
|
|
canShowReactionsCount?: boolean;
|
|
canBuyPremium?: boolean;
|
|
canShowReactionList?: boolean;
|
|
customEmojiSetsInfo?: ApiStickerSetInfo[];
|
|
customEmojiSets?: ApiStickerSet[];
|
|
canUnpin?: boolean;
|
|
canDelete?: boolean;
|
|
canReport?: boolean;
|
|
canEdit?: boolean;
|
|
canAppendTodoList?: boolean;
|
|
canForward?: boolean;
|
|
canFaveSticker?: boolean;
|
|
canUnfaveSticker?: boolean;
|
|
canCopy?: boolean;
|
|
canTranslate?: boolean;
|
|
canShowOriginal?: boolean;
|
|
isMessageTranslated?: boolean;
|
|
canSelectLanguage?: boolean;
|
|
isPrivate?: boolean;
|
|
isCurrentUserPremium?: boolean;
|
|
hasFullInfo?: boolean;
|
|
canCopyLink?: boolean;
|
|
canSelect?: boolean;
|
|
canDownload?: boolean;
|
|
canSaveGif?: boolean;
|
|
canRevote?: boolean;
|
|
canClosePoll?: boolean;
|
|
canLoadReadDate?: boolean;
|
|
shouldRenderShowWhen?: boolean;
|
|
activeDownloads: ActiveDownloads;
|
|
canShowSeenBy?: boolean;
|
|
enabledReactions?: ApiChatReactions;
|
|
canScheduleUntilOnline?: boolean;
|
|
reactionsLimit?: number;
|
|
canPlayAnimatedEmojis?: boolean;
|
|
isReactionPickerOpen?: boolean;
|
|
isInSavedMessages?: boolean;
|
|
isChannel?: boolean;
|
|
canReplyInChat?: boolean;
|
|
isWithPaidReaction?: boolean;
|
|
userFullName?: string;
|
|
canGift?: boolean;
|
|
savedDialogId?: string;
|
|
noForwardsMyEnabled?: boolean;
|
|
noForwardsPeerEnabled?: boolean;
|
|
};
|
|
|
|
const selection = window.getSelection();
|
|
const UNQUOTABLE_OFFSET = -1;
|
|
|
|
const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|
threadId,
|
|
availableReactions,
|
|
topReactions,
|
|
defaultTagReactions,
|
|
isOpen,
|
|
messageListType,
|
|
message,
|
|
customEmojiSetsInfo,
|
|
customEmojiSets,
|
|
album,
|
|
poll,
|
|
webPage,
|
|
story,
|
|
anchor,
|
|
targetHref,
|
|
noOptions,
|
|
canSendNow,
|
|
hasFullInfo,
|
|
canReschedule,
|
|
canReply,
|
|
canPin,
|
|
repliesThreadInfo,
|
|
canUnpin,
|
|
canDelete,
|
|
canShowReactionsCount,
|
|
chat,
|
|
canReport,
|
|
canShowReactionList,
|
|
canEdit,
|
|
canAppendTodoList,
|
|
enabledReactions,
|
|
reactionsLimit,
|
|
isPrivate,
|
|
isCurrentUserPremium,
|
|
canForward,
|
|
canBuyPremium,
|
|
canFaveSticker,
|
|
canUnfaveSticker,
|
|
canCopy,
|
|
canCopyLink,
|
|
canSelect,
|
|
canDownload,
|
|
canSaveGif,
|
|
canRevote,
|
|
canClosePoll,
|
|
canPlayAnimatedEmojis,
|
|
canLoadReadDate,
|
|
shouldRenderShowWhen,
|
|
activeDownloads,
|
|
noReplies,
|
|
canShowSeenBy,
|
|
canScheduleUntilOnline,
|
|
canTranslate,
|
|
isMessageTranslated,
|
|
canShowOriginal,
|
|
canSelectLanguage,
|
|
isReactionPickerOpen,
|
|
isInSavedMessages,
|
|
canReplyInChat,
|
|
isWithPaidReaction,
|
|
userFullName,
|
|
canGift,
|
|
className,
|
|
savedDialogId,
|
|
noForwardsMyEnabled,
|
|
noForwardsPeerEnabled,
|
|
onClose,
|
|
onCloseAnimationEnd,
|
|
}) => {
|
|
const {
|
|
openThread,
|
|
updateDraftReplyInfo,
|
|
setEditingId,
|
|
pinMessage,
|
|
openForwardMenu,
|
|
openReplyMenu,
|
|
faveSticker,
|
|
unfaveSticker,
|
|
toggleMessageSelection,
|
|
sendScheduledMessages,
|
|
rescheduleMessage,
|
|
downloadMedia,
|
|
cancelMediaDownload,
|
|
loadSeenBy,
|
|
openSeenByModal,
|
|
openReactorListModal,
|
|
loadFullChat,
|
|
loadReactors,
|
|
copyMessagesByIds,
|
|
saveGif,
|
|
loadStickers,
|
|
cancelPollVote,
|
|
closePoll,
|
|
toggleReaction,
|
|
requestMessageTranslation,
|
|
showOriginalMessage,
|
|
openChatLanguageModal,
|
|
openMessageReactionPicker,
|
|
openPremiumModal,
|
|
loadOutboxReadDate,
|
|
copyMessageLink,
|
|
openDeleteMessageModal,
|
|
addLocalPaidReaction,
|
|
openPaidReactionModal,
|
|
reportMessages,
|
|
openTodoListModal,
|
|
showNotification,
|
|
} = getActions();
|
|
|
|
const oldLang = useOldLang();
|
|
const lang = useLang();
|
|
|
|
const noForwardsNotice = noForwardsPeerEnabled
|
|
? lang('ContextMenuNoForwardsPeer', { name: userFullName })
|
|
: (noForwardsMyEnabled ? lang('ContextMenuNoForwardsYou') : undefined);
|
|
|
|
const { ref: containerRef } = useShowTransition({
|
|
isOpen,
|
|
onCloseAnimationEnd,
|
|
className: false,
|
|
});
|
|
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
|
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
|
|
const [isClosePollDialogOpen, openClosePollDialog, closeClosePollDialog] = useFlag();
|
|
const [selectionQuoteOffset, setSelectionQuoteOffset] = useState(UNQUOTABLE_OFFSET);
|
|
const [requestCalendar, calendar] = useSchedule(
|
|
canScheduleUntilOnline,
|
|
onClose,
|
|
message.date,
|
|
message.scheduleRepeatPeriod,
|
|
);
|
|
|
|
// `undefined` indicates that emoji are present and loading
|
|
const hasCustomEmoji = customEmojiSetsInfo === undefined || Boolean(customEmojiSetsInfo.length);
|
|
|
|
useEffect(() => {
|
|
if (canShowSeenBy && isOpen) {
|
|
loadSeenBy({ chatId: message.chatId, messageId: message.id });
|
|
}
|
|
}, [loadSeenBy, isOpen, message.chatId, message.id, canShowSeenBy]);
|
|
|
|
useEffect(() => {
|
|
if (canLoadReadDate && isOpen) {
|
|
loadOutboxReadDate({ chatId: message.chatId, messageId: message.id });
|
|
}
|
|
}, [canLoadReadDate, isOpen, message.chatId, message.id, message.readDate]);
|
|
|
|
useEffect(() => {
|
|
if (canShowReactionsCount && isOpen) {
|
|
loadReactors({ chatId: message.chatId, messageId: message.id });
|
|
}
|
|
}, [canShowReactionsCount, isOpen, loadReactors, message.chatId, message.id]);
|
|
|
|
useEffect(() => {
|
|
if (customEmojiSetsInfo?.length && customEmojiSets?.length !== customEmojiSetsInfo.length) {
|
|
customEmojiSetsInfo.forEach((set) => {
|
|
loadStickers({ stickerSetInfo: set });
|
|
});
|
|
}
|
|
}, [customEmojiSetsInfo, customEmojiSets, loadStickers]);
|
|
|
|
useEffect(() => {
|
|
if (!hasFullInfo && !isPrivate && isOpen) {
|
|
loadFullChat({ chatId: message.chatId });
|
|
}
|
|
}, [hasFullInfo, isOpen, isPrivate, loadFullChat, message.chatId]);
|
|
|
|
const seenByRecentPeers = useMemo(() => {
|
|
// No need for expensive global updates on chats or users, so we avoid them
|
|
const chatsById = getGlobal().chats.byId;
|
|
const usersById = getGlobal().users.byId;
|
|
if (message.reactions?.recentReactions?.length) {
|
|
const uniqueReactors = new Set(message.reactions?.recentReactions?.map(
|
|
({ peerId }) => usersById[peerId] || chatsById[peerId],
|
|
));
|
|
|
|
return Array.from(uniqueReactors).filter(Boolean).slice(0, PREVIEW_AVATAR_COUNT);
|
|
}
|
|
|
|
if (!message.seenByDates) {
|
|
return undefined;
|
|
}
|
|
|
|
return Object.keys(message.seenByDates).slice(0, PREVIEW_AVATAR_COUNT)
|
|
.map((id) => usersById[id] || chatsById[id])
|
|
.filter(Boolean);
|
|
}, [message.reactions?.recentReactions, message.seenByDates]);
|
|
|
|
const isDownloading = useMemo(() => {
|
|
const global = getGlobal();
|
|
if (album) {
|
|
return album.messages.some((msg) => {
|
|
const downloadableMedia = selectMessageDownloadableMedia(global, msg);
|
|
if (!downloadableMedia) return false;
|
|
return getIsDownloading(activeDownloads, downloadableMedia);
|
|
});
|
|
}
|
|
|
|
const downloadableMedia = selectMessageDownloadableMedia(global, message);
|
|
if (!downloadableMedia) return false;
|
|
return getIsDownloading(activeDownloads, downloadableMedia);
|
|
}, [activeDownloads, album, message]);
|
|
|
|
const selectionRange = canReply && selection?.rangeCount ? selection.getRangeAt(0) : undefined;
|
|
|
|
useEffect(() => {
|
|
if (isMessageTranslated) {
|
|
setSelectionQuoteOffset(UNQUOTABLE_OFFSET);
|
|
return;
|
|
}
|
|
|
|
const isMessageTextSelected = selectionRange
|
|
&& !selectionRange.collapsed
|
|
&& Boolean(message.content.text?.text)
|
|
&& isSelectionRangeInsideMessage(selectionRange);
|
|
|
|
if (!isMessageTextSelected) {
|
|
setSelectionQuoteOffset(UNQUOTABLE_OFFSET);
|
|
return;
|
|
}
|
|
|
|
const selectionText = getSelectionAsFormattedText(selectionRange);
|
|
|
|
const messageText = message.content.text!.text.replace(/\u00A0/g, ' ');
|
|
|
|
const canQuote = selectionText.text.trim().length > 0
|
|
&& messageText.includes(selectionText.text);
|
|
if (!canQuote) {
|
|
setSelectionQuoteOffset(UNQUOTABLE_OFFSET);
|
|
return;
|
|
}
|
|
|
|
setSelectionQuoteOffset(selectionRange.startOffset);
|
|
}, [
|
|
selectionRange, selectionRange?.collapsed, selectionRange?.startOffset, selectionRange?.endOffset,
|
|
isMessageTranslated, message.content.text,
|
|
]);
|
|
|
|
const closeMenu = useLastCallback(() => {
|
|
setIsMenuOpen(false);
|
|
onClose();
|
|
});
|
|
|
|
const handleDelete = useLastCallback(() => {
|
|
setIsMenuOpen(false);
|
|
closeMenu();
|
|
const messageIds = album?.messages
|
|
? album.messages.map(({ id }) => id)
|
|
: [message.id];
|
|
openDeleteMessageModal({
|
|
chatId: message.chatId,
|
|
messageIds,
|
|
isSchedule: messageListType === 'scheduled',
|
|
});
|
|
});
|
|
|
|
const closePinModal = useLastCallback(() => {
|
|
setIsPinModalOpen(false);
|
|
onClose();
|
|
});
|
|
|
|
const handleReply = useLastCallback(() => {
|
|
const quoteText = selectionQuoteOffset !== UNQUOTABLE_OFFSET && selectionRange
|
|
? getSelectionAsFormattedText(selectionRange) : undefined;
|
|
if (!canReplyInChat) {
|
|
openReplyMenu({
|
|
fromChatId: message.chatId, messageId: message.id, quoteText, quoteOffset: selectionQuoteOffset,
|
|
});
|
|
} else {
|
|
updateDraftReplyInfo({
|
|
replyToMsgId: message.id,
|
|
quoteText,
|
|
quoteOffset: selectionQuoteOffset,
|
|
monoforumPeerId: savedDialogId,
|
|
replyToPeerId: undefined,
|
|
});
|
|
}
|
|
closeMenu();
|
|
});
|
|
|
|
const handleOpenThread = useLastCallback(() => {
|
|
openThread({
|
|
chatId: message.chatId,
|
|
threadId: message.id,
|
|
});
|
|
closeMenu();
|
|
});
|
|
|
|
const handleEdit = useLastCallback(() => {
|
|
if (message.content.todo) {
|
|
openTodoListModal({
|
|
chatId: message.chatId,
|
|
messageId: message.id,
|
|
});
|
|
} else {
|
|
setEditingId({ messageId: message.id });
|
|
}
|
|
closeMenu();
|
|
});
|
|
|
|
const handleAppendTodoList = useLastCallback(() => {
|
|
if (!isCurrentUserPremium) {
|
|
showNotification({
|
|
message: lang('SubscribeToTelegramPremiumForAppendToDo'),
|
|
action: {
|
|
action: 'openPremiumModal',
|
|
payload: { initialSection: 'todo' },
|
|
},
|
|
actionText: oldLang('PremiumMore'),
|
|
});
|
|
} else {
|
|
openTodoListModal({
|
|
chatId: message.chatId,
|
|
messageId: message.id,
|
|
forNewTask: true,
|
|
});
|
|
}
|
|
closeMenu();
|
|
});
|
|
|
|
const handlePin = useLastCallback(() => {
|
|
setIsMenuOpen(false);
|
|
setIsPinModalOpen(true);
|
|
});
|
|
|
|
const handleUnpin = useLastCallback(() => {
|
|
pinMessage({ chatId: message.chatId, messageId: message.id, isUnpin: true });
|
|
closeMenu();
|
|
});
|
|
|
|
const handleForward = useLastCallback(() => {
|
|
closeMenu();
|
|
if (album?.messages) {
|
|
const messageIds = album.messages.map(({ id }) => id);
|
|
openForwardMenu({ fromChatId: message.chatId, messageIds });
|
|
} else {
|
|
openForwardMenu({ fromChatId: message.chatId, messageIds: [message.id] });
|
|
}
|
|
});
|
|
|
|
const handleFaveSticker = useLastCallback(() => {
|
|
closeMenu();
|
|
faveSticker({ sticker: message.content.sticker! });
|
|
});
|
|
|
|
const handleUnfaveSticker = useLastCallback(() => {
|
|
closeMenu();
|
|
unfaveSticker({ sticker: message.content.sticker! });
|
|
});
|
|
|
|
const handleCancelVote = useLastCallback(() => {
|
|
closeMenu();
|
|
cancelPollVote({ chatId: message.chatId, messageId: message.id });
|
|
});
|
|
|
|
const handlePollClose = useLastCallback(() => {
|
|
closeMenu();
|
|
closePoll({ chatId: message.chatId, messageId: message.id });
|
|
});
|
|
|
|
const handleSelectMessage = useLastCallback(() => {
|
|
const params = album?.messages
|
|
? {
|
|
messageId: message.id,
|
|
childMessageIds: album.messages.map(({ id }) => id),
|
|
withShift: false,
|
|
}
|
|
: { messageId: message.id, withShift: false };
|
|
|
|
toggleMessageSelection(params);
|
|
closeMenu();
|
|
});
|
|
|
|
const handleScheduledMessageSend = useLastCallback(() => {
|
|
sendScheduledMessages({ chatId: message.chatId, id: message.id });
|
|
closeMenu();
|
|
});
|
|
|
|
const handleRescheduleMessage = useLastCallback((
|
|
scheduledAt: number,
|
|
scheduleRepeatPeriod?: number,
|
|
) => {
|
|
rescheduleMessage({
|
|
chatId: message.chatId,
|
|
messageId: message.id,
|
|
scheduledAt,
|
|
scheduleRepeatPeriod,
|
|
});
|
|
onClose();
|
|
});
|
|
|
|
const handleOpenCalendar = useLastCallback(() => {
|
|
setIsMenuOpen(false);
|
|
requestCalendar(handleRescheduleMessage);
|
|
});
|
|
|
|
const handleOpenSeenByModal = useLastCallback(() => {
|
|
closeMenu();
|
|
openSeenByModal({ chatId: message.chatId, messageId: message.id });
|
|
});
|
|
|
|
const handleOpenReactorListModal = useLastCallback(() => {
|
|
closeMenu();
|
|
openReactorListModal({ chatId: message.chatId, messageId: message.id });
|
|
});
|
|
|
|
const handleCopyMessages = useLastCallback((messageIds: number[]) => {
|
|
copyMessagesByIds({ messageIds });
|
|
closeMenu();
|
|
});
|
|
|
|
const handleCopyLink = useLastCallback(() => {
|
|
copyMessageLink({
|
|
chatId: message.chatId,
|
|
messageId: message.id,
|
|
shouldIncludeThread: threadId !== MAIN_THREAD_ID,
|
|
shouldIncludeGrouped: true, // TODO: Provide correct value when ability to target specific message is added
|
|
});
|
|
closeMenu();
|
|
});
|
|
|
|
const handleCopyNumber = useLastCallback(() => {
|
|
copyTextToClipboard(message.content.contact!.phoneNumber);
|
|
closeMenu();
|
|
});
|
|
|
|
const handleDownloadClick = useLastCallback(() => {
|
|
const global = getGlobal();
|
|
(album?.messages || [message]).forEach((msg) => {
|
|
const downloadableMedia = selectMessageDownloadableMedia(global, msg);
|
|
if (!downloadableMedia) return;
|
|
|
|
if (isDownloading) {
|
|
cancelMediaDownload({ media: downloadableMedia });
|
|
} else {
|
|
downloadMedia({ media: downloadableMedia, originMessage: msg });
|
|
}
|
|
});
|
|
closeMenu();
|
|
});
|
|
|
|
const handleSaveGif = useLastCallback(() => {
|
|
const video = getMessageVideo(message);
|
|
saveGif({ gif: video! });
|
|
closeMenu();
|
|
});
|
|
|
|
const handleToggleReaction = useLastCallback((reaction: ApiReaction) => {
|
|
if (isInSavedMessages && !isCurrentUserPremium) {
|
|
openPremiumModal({
|
|
initialSection: 'saved_tags',
|
|
});
|
|
} else {
|
|
toggleReaction({
|
|
chatId: message.chatId, messageId: message.id, reaction, shouldAddToRecent: true,
|
|
});
|
|
}
|
|
closeMenu();
|
|
});
|
|
|
|
const handleSendPaidReaction = useLastCallback(() => {
|
|
addLocalPaidReaction({
|
|
chatId: message.chatId, messageId: message.id, count: 1,
|
|
});
|
|
closeMenu();
|
|
});
|
|
|
|
const handlePaidReactionModalOpen = useLastCallback(() => {
|
|
openPaidReactionModal({
|
|
chatId: message.chatId,
|
|
messageId: message.id,
|
|
});
|
|
|
|
closeMenu();
|
|
});
|
|
|
|
const handleReactionPickerOpen = useLastCallback((position: IAnchorPosition) => {
|
|
openMessageReactionPicker({ chatId: message.chatId, messageId: message.id, position });
|
|
});
|
|
|
|
const handleTranslate = useLastCallback(() => {
|
|
requestMessageTranslation({
|
|
chatId: message.chatId,
|
|
id: message.id,
|
|
});
|
|
closeMenu();
|
|
});
|
|
|
|
const handleShowOriginal = useLastCallback(() => {
|
|
showOriginalMessage({
|
|
chatId: message.chatId,
|
|
id: message.id,
|
|
});
|
|
closeMenu();
|
|
});
|
|
|
|
const handleSelectLanguage = useLastCallback(() => {
|
|
openChatLanguageModal({
|
|
chatId: message.chatId,
|
|
messageId: message.id,
|
|
});
|
|
closeMenu();
|
|
});
|
|
|
|
const reportMessageIds = useMemo(() => (album ? album.messages : [message]).map(({ id }) => id), [album, message]);
|
|
|
|
const handleReport = useLastCallback(() => {
|
|
if (!chat) return;
|
|
setIsMenuOpen(false);
|
|
onClose();
|
|
reportMessages({
|
|
chatId: chat.id, messageIds: reportMessageIds,
|
|
});
|
|
});
|
|
|
|
if (noOptions) {
|
|
closeMenu();
|
|
|
|
return undefined;
|
|
}
|
|
|
|
return (
|
|
<div ref={containerRef} className={buildClassName('ContextMenuContainer', className)}>
|
|
<MessageContextMenu
|
|
isReactionPickerOpen={isReactionPickerOpen}
|
|
availableReactions={availableReactions}
|
|
topReactions={topReactions}
|
|
defaultTagReactions={defaultTagReactions}
|
|
isWithPaidReaction={isWithPaidReaction}
|
|
message={message}
|
|
isPrivate={isPrivate}
|
|
isCurrentUserPremium={isCurrentUserPremium}
|
|
canBuyPremium={canBuyPremium}
|
|
isOpen={isMenuOpen}
|
|
enabledReactions={enabledReactions}
|
|
reactionsLimit={reactionsLimit}
|
|
anchor={anchor}
|
|
targetHref={targetHref}
|
|
canShowReactionsCount={canShowReactionsCount}
|
|
canShowReactionList={canShowReactionList}
|
|
canSendNow={canSendNow}
|
|
canReschedule={canReschedule}
|
|
canReply={canReply}
|
|
canQuote={selectionQuoteOffset !== UNQUOTABLE_OFFSET}
|
|
canDelete={canDelete}
|
|
canPin={canPin}
|
|
canReport={canReport}
|
|
repliesThreadInfo={repliesThreadInfo}
|
|
canUnpin={canUnpin}
|
|
canEdit={canEdit}
|
|
canAppendTodoList={canAppendTodoList}
|
|
canForward={canForward}
|
|
canFaveSticker={canFaveSticker}
|
|
canUnfaveSticker={canUnfaveSticker}
|
|
canCopy={canCopy}
|
|
canCopyLink={canCopyLink}
|
|
canSelect={canSelect}
|
|
canDownload={canDownload}
|
|
canSaveGif={canSaveGif}
|
|
canRevote={canRevote}
|
|
canClosePoll={canClosePoll}
|
|
canShowSeenBy={canShowSeenBy}
|
|
canTranslate={canTranslate}
|
|
canShowOriginal={canShowOriginal}
|
|
canSelectLanguage={canSelectLanguage}
|
|
canPlayAnimatedEmojis={canPlayAnimatedEmojis}
|
|
shouldRenderShowWhen={shouldRenderShowWhen}
|
|
canLoadReadDate={canLoadReadDate}
|
|
hasCustomEmoji={hasCustomEmoji}
|
|
customEmojiSets={customEmojiSets}
|
|
isDownloading={isDownloading}
|
|
seenByRecentPeers={seenByRecentPeers}
|
|
isInSavedMessages={isInSavedMessages}
|
|
noReplies={noReplies}
|
|
poll={poll}
|
|
webPage={webPage}
|
|
story={story}
|
|
onOpenThread={handleOpenThread}
|
|
onReply={handleReply}
|
|
onEdit={handleEdit}
|
|
onAppendTodoList={handleAppendTodoList}
|
|
onPin={handlePin}
|
|
onUnpin={handleUnpin}
|
|
onForward={handleForward}
|
|
onDelete={handleDelete}
|
|
onReport={handleReport}
|
|
onFaveSticker={handleFaveSticker}
|
|
onUnfaveSticker={handleUnfaveSticker}
|
|
onSelect={handleSelectMessage}
|
|
onSend={handleScheduledMessageSend}
|
|
onReschedule={handleOpenCalendar}
|
|
onClose={closeMenu}
|
|
onCopyLink={handleCopyLink}
|
|
onCopyMessages={handleCopyMessages}
|
|
onCopyNumber={handleCopyNumber}
|
|
onDownload={handleDownloadClick}
|
|
onSaveGif={handleSaveGif}
|
|
onCancelVote={handleCancelVote}
|
|
onClosePoll={openClosePollDialog}
|
|
onShowSeenBy={handleOpenSeenByModal}
|
|
onToggleReaction={handleToggleReaction}
|
|
onSendPaidReaction={handleSendPaidReaction}
|
|
onShowPaidReactionModal={handlePaidReactionModalOpen}
|
|
onShowReactors={handleOpenReactorListModal}
|
|
onReactionPickerOpen={handleReactionPickerOpen}
|
|
onTranslate={handleTranslate}
|
|
onShowOriginal={handleShowOriginal}
|
|
onSelectLanguage={handleSelectLanguage}
|
|
userFullName={userFullName}
|
|
canGift={canGift}
|
|
noForwardsNotice={noForwardsNotice}
|
|
/>
|
|
<PinMessageModal
|
|
isOpen={isPinModalOpen}
|
|
messageId={message.id}
|
|
chatId={message.chatId}
|
|
onClose={closePinModal}
|
|
/>
|
|
<ConfirmDialog
|
|
isOpen={isClosePollDialogOpen}
|
|
onClose={closeClosePollDialog}
|
|
text={oldLang('lng_polls_stop_warning')}
|
|
confirmLabel={oldLang('lng_polls_stop_sure')}
|
|
confirmHandler={handlePollClose}
|
|
/>
|
|
{canReschedule && calendar}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default memo(withGlobal<OwnProps>(
|
|
(global, { message, messageListType, detectedLanguage }): Complete<StateProps> => {
|
|
const { threadId } = selectCurrentMessageList(global) || {};
|
|
|
|
const { defaultTags, topReactions, availableReactions } = global.reactions;
|
|
|
|
const activeDownloads = selectActiveDownloads(global);
|
|
const chat = selectChat(global, message.chatId);
|
|
const isPrivate = chat && isUserId(chat.id);
|
|
const chatFullInfo = !isPrivate ? selectChatFullInfo(global, message.chatId) : undefined;
|
|
const user = selectUser(global, message.chatId);
|
|
const userFullName = user && getUserFullName(user);
|
|
const userFullInfo = isPrivate ? selectUserFullInfo(global, message.chatId) : undefined;
|
|
|
|
const {
|
|
seenByExpiresAt, seenByMaxChatMembers, maxUniqueReactions, readDateExpiresAt,
|
|
} = global.appConfig;
|
|
|
|
const reactionsLimit = chatFullInfo?.reactionsLimit || maxUniqueReactions;
|
|
|
|
const {
|
|
noOptions,
|
|
canReplyGlobally,
|
|
canPin,
|
|
canUnpin,
|
|
canDelete,
|
|
canReport,
|
|
canEdit,
|
|
canFaveSticker,
|
|
canUnfaveSticker,
|
|
canCopy,
|
|
canCopyLink,
|
|
canSelect,
|
|
canDownload,
|
|
canSaveGif,
|
|
canRevote,
|
|
canClosePoll,
|
|
} = (threadId && selectAllowedMessageActionsSlow(global, message, threadId)) || {};
|
|
const canForward = selectCanForwardMessage(global, message);
|
|
|
|
const userStatus = isPrivate ? selectUserStatus(global, chat.id) : undefined;
|
|
const isOwn = isOwnMessage(message);
|
|
const chatBot = chat && selectBot(global, chat.id);
|
|
const isBot = Boolean(chatBot);
|
|
const isMessageUnread = selectIsMessageUnread(
|
|
global, message.chatId, threadId || MAIN_THREAD_ID, message.id, messageListType,
|
|
);
|
|
const canLoadReadDate = Boolean(
|
|
isPrivate
|
|
&& isOwn
|
|
&& !isBot
|
|
&& !isMessageUnread
|
|
&& readDateExpiresAt
|
|
&& message.date > Date.now() / 1000 - readDateExpiresAt
|
|
&& !userStatus?.isReadDateRestricted
|
|
&& messageListType !== 'scheduled',
|
|
);
|
|
const shouldRenderShowWhen = Boolean(
|
|
canLoadReadDate && isPrivate && selectUserStatus(global, chat.id)?.isReadDateRestrictedByMe,
|
|
);
|
|
const isPinned = messageListType === 'pinned';
|
|
const isScheduled = messageListType === 'scheduled';
|
|
const isChannel = chat && isChatChannel(chat);
|
|
|
|
const threadInfo = threadId && selectThreadInfo(global, message.chatId, threadId);
|
|
const isMessageThread = Boolean(threadInfo && !threadInfo?.isCommentsInfo && threadInfo?.fromChannelId);
|
|
const topic = threadId ? selectTopic(global, message.chatId, threadId) : undefined;
|
|
|
|
const canSendText = chat && !isUserRightBanned(chat, 'sendPlain', chatFullInfo);
|
|
|
|
const canReplyInChat = chat && threadId ? getCanPostInChat(chat, topic, isMessageThread, chatFullInfo)
|
|
&& canSendText : false;
|
|
|
|
const isLocal = isMessageLocal(message);
|
|
const hasTtl = hasMessageTtl(message);
|
|
const canShowSeenBy = Boolean(!isLocal
|
|
&& chat
|
|
&& !chat.isMonoforum
|
|
&& !isMessageUnread
|
|
&& seenByMaxChatMembers
|
|
&& seenByExpiresAt
|
|
&& isChatGroup(chat)
|
|
&& isOwn
|
|
&& !isScheduled
|
|
&& chat.membersCount
|
|
&& chat.membersCount <= seenByMaxChatMembers
|
|
&& message.date > Date.now() / 1000 - seenByExpiresAt);
|
|
const isAction = isActionMessage(message);
|
|
const canShowReactionsCount = !isLocal && !isChannel && !isScheduled && !isAction && !isPrivate && message.reactions
|
|
&& !areReactionsEmpty(message.reactions) && message.reactions.canSeeList;
|
|
const isProtected = selectIsMessageProtected(global, message);
|
|
const canCopyNumber = Boolean(message.content.contact);
|
|
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
|
|
|
const customEmojiSetsInfo = selectMessageCustomEmojiSets(global, message);
|
|
const customEmojiSetsNotFiltered = customEmojiSetsInfo?.map((set) => selectStickerSet(global, set));
|
|
const customEmojiSets = customEmojiSetsNotFiltered?.every<ApiStickerSet>(Boolean)
|
|
? customEmojiSetsNotFiltered : undefined;
|
|
|
|
const translationRequestLanguage = selectRequestedMessageTranslationLanguage(global, message.chatId, message.id);
|
|
const hasTranslation = translationRequestLanguage
|
|
? Boolean(selectMessageTranslations(global, message.chatId, translationRequestLanguage)[message.id]?.text)
|
|
: undefined;
|
|
const canTranslate = !hasTranslation && selectCanTranslateMessage(global, message, detectedLanguage);
|
|
const isChatTranslated = selectRequestedChatTranslationLanguage(global, message.chatId);
|
|
|
|
const isInSavedMessages = selectIsChatWithSelf(global, message.chatId);
|
|
|
|
const poll = selectPollFromMessage(global, message);
|
|
const webPage = selectWebPageFromMessage(global, message);
|
|
const storyData = message.content.storyData;
|
|
const story = storyData ? selectPeerStory(global, storyData.peerId, storyData.id) : undefined;
|
|
|
|
const canGift = selectCanGift(global, message.chatId);
|
|
|
|
const savedDialogId = selectSavedDialogIdFromMessage(global, message);
|
|
const todoItemsMax = global.appConfig.todoItemsMax;
|
|
const canAppendTodoList = message.content.todo?.todo.othersCanAppend
|
|
&& message.content.todo?.todo.items?.length < todoItemsMax;
|
|
|
|
return {
|
|
threadId,
|
|
chat,
|
|
availableReactions,
|
|
topReactions,
|
|
defaultTagReactions: defaultTags,
|
|
noOptions,
|
|
canReport,
|
|
canSendNow: isScheduled,
|
|
canReschedule: isScheduled,
|
|
canReply: !isPinned && !isScheduled && canReplyGlobally,
|
|
canPin: !isScheduled && canPin,
|
|
canUnpin: !isScheduled && canUnpin,
|
|
canDelete,
|
|
canEdit: !isPinned && canEdit,
|
|
canAppendTodoList,
|
|
canForward: !isScheduled && canForward,
|
|
canFaveSticker: !isScheduled && canFaveSticker,
|
|
canUnfaveSticker: !isScheduled && canUnfaveSticker,
|
|
canCopy: (canCopyNumber || (!isProtected && canCopy)),
|
|
canCopyLink: !isScheduled && canCopyLink,
|
|
canSelect,
|
|
canDownload: !isProtected && canDownload,
|
|
canSaveGif: !isProtected && canSaveGif,
|
|
canRevote,
|
|
canClosePoll: !isScheduled && canClosePoll,
|
|
activeDownloads,
|
|
canShowSeenBy,
|
|
canLoadReadDate,
|
|
shouldRenderShowWhen,
|
|
enabledReactions: chat?.isForbidden ? undefined : chatFullInfo?.enabledReactions,
|
|
reactionsLimit,
|
|
isPrivate,
|
|
isCurrentUserPremium,
|
|
hasFullInfo: Boolean(chatFullInfo),
|
|
canShowReactionsCount,
|
|
canShowReactionList: !isLocal && !isAction && !isScheduled && !hasTtl,
|
|
canBuyPremium: !isCurrentUserPremium && !selectIsPremiumPurchaseBlocked(global),
|
|
customEmojiSetsInfo,
|
|
customEmojiSets,
|
|
canScheduleUntilOnline: selectCanScheduleUntilOnline(global, message.chatId),
|
|
canTranslate,
|
|
canShowOriginal: hasTranslation && !isChatTranslated,
|
|
canSelectLanguage: hasTranslation && !isChatTranslated,
|
|
isMessageTranslated: hasTranslation,
|
|
canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global),
|
|
isReactionPickerOpen: selectIsReactionPickerOpen(global),
|
|
isInSavedMessages,
|
|
isChannel,
|
|
canReplyInChat,
|
|
isWithPaidReaction: chatFullInfo?.isPaidReactionAvailable,
|
|
poll,
|
|
story,
|
|
userFullName,
|
|
canGift,
|
|
savedDialogId,
|
|
webPage,
|
|
noForwardsMyEnabled: userFullInfo?.noForwardsMyEnabled,
|
|
noForwardsPeerEnabled: userFullInfo?.noForwardsPeerEnabled,
|
|
};
|
|
},
|
|
)(ContextMenuContainer));
|