Message Context Menu: Fix message link copying in Safari (#4530)

This commit is contained in:
Alexander Zinchuk 2024-05-03 14:38:14 +02:00
parent 6def917d1c
commit 209564ca6f
4 changed files with 74 additions and 24 deletions

View File

@ -397,7 +397,13 @@ const MessageContextMenu: FC<OwnProps> = ({
<MenuItem icon="web" onClick={onSelectLanguage}>{lang('lng_settings_change_lang')}</MenuItem> <MenuItem icon="web" onClick={onSelectLanguage}>{lang('lng_settings_change_lang')}</MenuItem>
)} )}
{copyOptions.map((option) => ( {copyOptions.map((option) => (
<MenuItem key={option.label} icon={option.icon} onClick={option.handler}>{lang(option.label)}</MenuItem> <MenuItem
key={option.label}
icon={option.icon}
onClick={option.handler}
withPreventDefaultOnMouseDown
>{lang(option.label)}
</MenuItem>
))} ))}
{canPin && <MenuItem icon="pin" onClick={onPin}>{lang('DialogPin')}</MenuItem>} {canPin && <MenuItem icon="pin" onClick={onPin}>{lang('DialogPin')}</MenuItem>}
{canUnpin && <MenuItem icon="unpin" onClick={onUnpin}>{lang('DialogUnpin')}</MenuItem>} {canUnpin && <MenuItem icon="unpin" onClick={onUnpin}>{lang('DialogUnpin')}</MenuItem>}

View File

@ -27,6 +27,7 @@ export type MenuItemProps = {
destructive?: boolean; destructive?: boolean;
ariaLabel?: string; ariaLabel?: string;
withWrap?: boolean; withWrap?: boolean;
withPreventDefaultOnMouseDown?: boolean;
}; };
const MenuItem: FC<MenuItemProps> = (props) => { const MenuItem: FC<MenuItemProps> = (props) => {
@ -45,6 +46,7 @@ const MenuItem: FC<MenuItemProps> = (props) => {
withWrap, withWrap,
onContextMenu, onContextMenu,
clickArg, clickArg,
withPreventDefaultOnMouseDown,
} = props; } = props;
const lang = useLang(); const lang = useLang();
@ -74,6 +76,11 @@ const MenuItem: FC<MenuItemProps> = (props) => {
onClick(e, clickArg); onClick(e, clickArg);
}); });
const handleMouseDown = useLastCallback((e: React.SyntheticEvent<HTMLDivElement | HTMLAnchorElement>) => {
if (withPreventDefaultOnMouseDown) {
e.preventDefault();
}
});
const fullClassName = buildClassName( const fullClassName = buildClassName(
'MenuItem', 'MenuItem',
@ -110,6 +117,7 @@ const MenuItem: FC<MenuItemProps> = (props) => {
rel="noopener noreferrer" rel="noopener noreferrer"
dir={lang.isRtl ? 'rtl' : undefined} dir={lang.isRtl ? 'rtl' : undefined}
onClick={onClick} onClick={onClick}
onMouseDown={handleMouseDown}
> >
{content} {content}
</a> </a>
@ -123,6 +131,7 @@ const MenuItem: FC<MenuItemProps> = (props) => {
className={fullClassName} className={fullClassName}
onClick={handleClick} onClick={handleClick}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onMouseDown={handleMouseDown}
onContextMenu={onContextMenu} onContextMenu={onContextMenu}
aria-label={ariaLabel} aria-label={ariaLabel}
title={ariaLabel} title={ariaLabel}

View File

@ -33,7 +33,7 @@ import {
SUPPORTED_IMAGE_CONTENT_TYPES, SUPPORTED_IMAGE_CONTENT_TYPES,
SUPPORTED_VIDEO_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES,
} from '../../../config'; } from '../../../config';
import { copyTextToClipboard } from '../../../util/clipboard'; import { copyTextToClipboardFromPromise } from '../../../util/clipboard';
import { isDeepLink } from '../../../util/deepLinkParser'; import { isDeepLink } from '../../../util/deepLinkParser';
import { ensureProtocol } from '../../../util/ensureProtocol'; import { ensureProtocol } from '../../../util/ensureProtocol';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
@ -1962,35 +1962,25 @@ addActionHandler('copyMessageLink', async (global, actions, payload): Promise<vo
}); });
return; return;
} }
const showErrorOccurredNotification = () => actions.showNotification({
if (!isChatChannel(chat) && !isChatSuperGroup(chat)) { message: translate('ErrorOccurred'),
actions.showNotification({ tabId,
message: translate('lng_filters_link_private_error'),
tabId,
});
return;
}
const link = await callApi('exportMessageLink', {
chat,
id: messageId,
shouldIncludeThread,
shouldIncludeGrouped,
}); });
if (!link) { if (!isChatChannel(chat) && !isChatSuperGroup(chat)) {
actions.showNotification({ showErrorOccurredNotification();
message: translate('ErrorOccurred'),
tabId,
});
return; return;
} }
const showLinkCopiedNotification = () => actions.showNotification({
copyTextToClipboard(link);
actions.showNotification({
message: translate('LinkCopied'), message: translate('LinkCopied'),
tabId, tabId,
}); });
const callApiExportMessageLinkPromise = callApi('exportMessageLink', {
chat, id: messageId, shouldIncludeThread, shouldIncludeGrouped,
});
await copyTextToClipboardFromPromise(
callApiExportMessageLinkPromise, showLinkCopiedNotification, showErrorOccurredNotification,
);
}); });
function countSortedIds(ids: number[], from: number, to: number) { function countSortedIds(ids: number[], from: number, to: number) {

View File

@ -59,6 +59,51 @@ export const copyImageToClipboard = (imageUrl?: string) => {
imageEl.src = imageUrl; imageEl.src = imageUrl;
}; };
export const copyTextToClipboardFromPromise = async (
getTextPromise: Promise<string | undefined>,
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) { async function copyBlobToClipboard(pngBlob: Blob | null) {
if (!pngBlob || !CLIPBOARD_ITEM_SUPPORTED) { if (!pngBlob || !CLIPBOARD_ITEM_SUPPORTED) {
return; return;