diff --git a/src/components/middle/message/MessageContextMenu.tsx b/src/components/middle/message/MessageContextMenu.tsx index 3cc599ffa..e6985f035 100644 --- a/src/components/middle/message/MessageContextMenu.tsx +++ b/src/components/middle/message/MessageContextMenu.tsx @@ -397,7 +397,13 @@ const MessageContextMenu: FC = ({ {lang('lng_settings_change_lang')} )} {copyOptions.map((option) => ( - {lang(option.label)} + {lang(option.label)} + ))} {canPin && {lang('DialogPin')}} {canUnpin && {lang('DialogUnpin')}} diff --git a/src/components/ui/MenuItem.tsx b/src/components/ui/MenuItem.tsx index 86d73a1f8..ba238dd12 100644 --- a/src/components/ui/MenuItem.tsx +++ b/src/components/ui/MenuItem.tsx @@ -27,6 +27,7 @@ export type MenuItemProps = { destructive?: boolean; ariaLabel?: string; withWrap?: boolean; + withPreventDefaultOnMouseDown?: boolean; }; const MenuItem: FC = (props) => { @@ -45,6 +46,7 @@ const MenuItem: FC = (props) => { withWrap, onContextMenu, clickArg, + withPreventDefaultOnMouseDown, } = props; const lang = useLang(); @@ -74,6 +76,11 @@ const MenuItem: FC = (props) => { onClick(e, clickArg); }); + const handleMouseDown = useLastCallback((e: React.SyntheticEvent) => { + if (withPreventDefaultOnMouseDown) { + e.preventDefault(); + } + }); const fullClassName = buildClassName( 'MenuItem', @@ -110,6 +117,7 @@ const MenuItem: FC = (props) => { rel="noopener noreferrer" dir={lang.isRtl ? 'rtl' : undefined} onClick={onClick} + onMouseDown={handleMouseDown} > {content} @@ -123,6 +131,7 @@ const MenuItem: FC = (props) => { className={fullClassName} onClick={handleClick} onKeyDown={handleKeyDown} + onMouseDown={handleMouseDown} onContextMenu={onContextMenu} aria-label={ariaLabel} title={ariaLabel} diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 54f0e5dc5..932f31c5a 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -33,7 +33,7 @@ import { SUPPORTED_IMAGE_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES, } from '../../../config'; -import { copyTextToClipboard } from '../../../util/clipboard'; +import { copyTextToClipboardFromPromise } from '../../../util/clipboard'; import { isDeepLink } from '../../../util/deepLinkParser'; import { ensureProtocol } from '../../../util/ensureProtocol'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; @@ -1962,35 +1962,25 @@ addActionHandler('copyMessageLink', async (global, actions, payload): Promise actions.showNotification({ + message: translate('ErrorOccurred'), + tabId, }); - if (!link) { - actions.showNotification({ - message: translate('ErrorOccurred'), - tabId, - }); + if (!isChatChannel(chat) && !isChatSuperGroup(chat)) { + showErrorOccurredNotification(); return; } - - copyTextToClipboard(link); - actions.showNotification({ + const showLinkCopiedNotification = () => actions.showNotification({ message: translate('LinkCopied'), tabId, }); + const callApiExportMessageLinkPromise = callApi('exportMessageLink', { + chat, id: messageId, shouldIncludeThread, shouldIncludeGrouped, + }); + await copyTextToClipboardFromPromise( + callApiExportMessageLinkPromise, showLinkCopiedNotification, showErrorOccurredNotification, + ); }); function countSortedIds(ids: number[], from: number, to: number) { diff --git a/src/util/clipboard.ts b/src/util/clipboard.ts index 279fdc50e..9d0851dce 100644 --- a/src/util/clipboard.ts +++ b/src/util/clipboard.ts @@ -59,6 +59,51 @@ export const copyImageToClipboard = (imageUrl?: string) => { imageEl.src = imageUrl; }; +export const copyTextToClipboardFromPromise = async ( + getTextPromise: Promise, + onSuccess: NoneToVoidFunction, + onFailure: NoneToVoidFunction, +) => { + const copyTextToClipboardFallback = async () => { + try { + const text = await getTextPromise; + if (text) { + copyTextToClipboard(text); + } else { + onFailure(); + } + return Boolean(text); + } catch { + onFailure(); + return false; + } + }; + if (!CLIPBOARD_ITEM_SUPPORTED || !navigator.clipboard.write) { + if (await copyTextToClipboardFallback()) onSuccess(); + return; + } + try { + let hasGetDataError = false; + const rejectGetDataError = () => Promise.reject(new Error('GET_DATA_ERROR')); + + const clipboardTextItem = new ClipboardItem({ + 'text/plain': getTextPromise.then((text) => text || rejectGetDataError()).catch(() => { + hasGetDataError = true; + return ''; + }), + }); + await navigator.clipboard.write([clipboardTextItem]); + if (hasGetDataError) { + onFailure(); + return; + } + } catch { + // Promises in ClipboardItem aren't supported in older Chrome versions + if (!await copyTextToClipboardFallback()) return; + } + onSuccess(); +}; + async function copyBlobToClipboard(pngBlob: Blob | null) { if (!pngBlob || !CLIPBOARD_ITEM_SUPPORTED) { return;