diff --git a/packages/presentation/src/i18n/resources.ts b/packages/presentation/src/i18n/resources.ts index 2a8676be7..261f88bd8 100644 --- a/packages/presentation/src/i18n/resources.ts +++ b/packages/presentation/src/i18n/resources.ts @@ -93,23 +93,32 @@ export default { 'share-url.clipboard.status_success': 'The URL is copied to the clipboard', /** The status of the clipboard operation when copying a URL */ 'share-url.clipboard.status_unsupported': 'Clipboard not supported', - /** The share URL menu item text for copying a link */ - 'share-url.menu-item.copy.text': 'Copy link', - /** The share URL menu item text for configuring a public share link */ - 'share-url.menu-item.share.text': 'Share link', /** The share URL menu item text for opening a preview window */ 'share-url.menu-item.open.text': 'Open preview', - /** The dialog header for the share url dialog */ - 'share-url.dialog.header': 'Configure share link', - /** Action in the share url dialog that creates a URL that can be used to preview the current pathname */ - 'share-url.dialog.action.enable-sharing': 'Enable sharing to those who have the link', - /** Disables sharing access to those who have the link */ - 'share-url.dialog.action.disable-sharing': 'Disable sharing', /** Error toast that notifies that URL Preview Secrets can't be generated as the user lacks ACL grants */ 'preview-url-secret.missing-grants': "You don't have permission to create URL Preview Secrets. This will likely cause the preview to fail loading.", /** The `aria-label` for the button that opens the share menu */ 'preview-frame.share-button.aria-label': 'Share this preview', - /** The tooltip for the button that opens the share menu */ - 'preview-frame.share-button.tooltip': 'Share this preview', + /** The for the QR Code SVG that shows a link to the current preview */ + 'share-preview-menu.qr-code.title': 'A QR Code which encodes the URL: {{url}}', + /** Error message toast that shows the current user does not have permission to toggle sharing of the current preview */ + 'share-preview-menu.error_toggle-sharing': + "You don't have permission to toggle sharing of this preview", + /** The text shown on the sharing toggle tooltip when sharing is enabled */ + 'share-preview-menu.toggle-button.tooltip_disable': 'Disable sharing', + /** The text shown on the sharing toggle tooltip when sharing is disabled */ + 'share-preview-menu.toggle-button.tooltip_enable': 'Enable sharing', + /** The first line of the label that renders next to the sharing toggle, it renders on two rows */ + 'share-preview-menu.toggle-button.label_first-line': 'Share this preview', + /** The second line of the label that renders next to the sharing toggle, it renders on two rows */ + 'share-preview-menu.toggle-button.label_second-line': 'with anyone who has the link', + /** Placeholder message for the QR Code SVG when sharing is yet to be enabled */ + 'share-preview-menu.qr-code.placeholder': 'QR code will appear here', + /** The text show below the QR Code SVG, with instructions on how to use it */ + 'share-preview-menu.qr-code.instructions': 'Scan the QR Code to open the preview on your phone.', + /** Menu item in the share preview menu that allows copying the current preview URL, if sharing is enabled */ + 'share-preview-menu.copy-url.text': 'Copy preview link', + /** Fallback message shown when the current user is not permitted to share previews */ + 'share-preview-menu.error_missing-grants': "You don't have permission to share previews. ", } diff --git a/packages/presentation/src/preview/QRCodeSVG.tsx b/packages/presentation/src/preview/QRCodeSVG.tsx index 7cad5ce96..68bc9af63 100644 --- a/packages/presentation/src/preview/QRCodeSVG.tsx +++ b/packages/presentation/src/preview/QRCodeSVG.tsx @@ -12,7 +12,6 @@ import {Ecc, QrCode, QrSegment} from './qrcodegen' type Modules = Array<Array<boolean>> type Excavation = {x: number; y: number; w: number; h: number} type ErrorCorrectionLevel = 'L' | 'M' | 'Q' | 'H' -type CrossOrigin = 'anonymous' | 'use-credentials' | '' | undefined type ERROR_LEVEL_MAPPED_TYPE = { [index in ErrorCorrectionLevel]: Ecc @@ -25,51 +24,6 @@ const ERROR_LEVEL_MAP: ERROR_LEVEL_MAPPED_TYPE = { H: Ecc.HIGH, } as const -type ImageSettings = { - /** - * The URI of the embedded image. - */ - src: string - /** - * The height, in pixels, of the image. - */ - height: number - /** - * The width, in pixels, of the image. - */ - width: number - /** - * Whether or not to "excavate" the modules around the embedded image. This - * means that any modules the embedded image overlaps will use the background - * color. - */ - excavate: boolean - /** - * The horiztonal offset of the embedded image, starting from the top left corner. - * Will center if not specified. - */ - x?: number - /** - * The vertical offset of the embedded image, starting from the top left corner. - * Will center if not specified. - */ - y?: number - /** - * The opacity of the embedded image in the range of 0-1. - * @defaultValue 1 - */ - opacity?: number - /** - * The cross-origin value to use when loading the image. This is used to - * ensure compatibility with CORS, particularly when extracting image data - * from QRCodeCanvas. - * Note: `undefined` is treated differently than the seemingly equivalent - * empty string. This is intended to align with HTML behavior where omitting - * the attribute behaves differently than the empty string. - */ - crossOrigin?: CrossOrigin -} - type QRProps = { /** * The value to encode into the QR Code. @@ -87,11 +41,9 @@ type QRProps = { */ level?: ErrorCorrectionLevel /** - * The foregtound color used to render the QR Code. - * @see https://developer.mozilla.org/en-US/docs/Web/CSS/color_value * @defaultValue #000000 */ - fgColor?: string + color?: string /** * The title to assign to the QR Code. Used for accessibility reasons. */ @@ -104,8 +56,7 @@ type QRProps = { * @defaultValue 1 */ minVersion?: number - imageSize: number - imageExcavate: boolean + logoSize?: number } const DEFAULT_SIZE = 128 @@ -117,12 +68,6 @@ const DEFAULT_MINVERSION = 1 const SPEC_MARGIN_SIZE = 4 const DEFAULT_MARGIN_SIZE = 0 -// This is *very* rough estimate of max amount of QRCode allowed to be covered. -// It is "wrong" in a lot of ways (area is a terrible way to estimate, it -// really should be number of modules covered), but if for some reason we don't -// get an explicit height or width, I'd rather default to something than throw. -const DEFAULT_IMG_SCALE = 0.1 - function generatePath(modules: Modules, margin: number = 0): string { const ops: Array<string> = [] modules.forEach(function (row, y) { @@ -181,40 +126,31 @@ function getImageSettings( cells: Modules, size: number, margin: number, - imageSettings?: ImageSettings, + logoSize?: number, ): null | { x: number y: number h: number w: number excavation: Excavation | null - opacity: number - crossOrigin: CrossOrigin } { - if (imageSettings == null) { + if (!logoSize) { return null } const numCells = cells.length + margin * 2 - const defaultSize = Math.floor(size * DEFAULT_IMG_SCALE) const scale = numCells / size - const w = (imageSettings.width || defaultSize) * scale - const h = (imageSettings.height || defaultSize) * scale - const x = imageSettings.x == null ? cells.length / 2 - w / 2 : imageSettings.x * scale - const y = imageSettings.y == null ? cells.length / 2 - h / 2 : imageSettings.y * scale - const opacity = imageSettings.opacity == null ? 1 : imageSettings.opacity - - let excavation = null - if (imageSettings.excavate) { - const floorX = Math.floor(x) - const floorY = Math.floor(y) - const ceilW = Math.ceil(w + x - floorX) - const ceilH = Math.ceil(h + y - floorY) - excavation = {x: floorX, y: floorY, w: ceilW, h: ceilH} - } + const w = logoSize * scale + const h = logoSize * scale + const x = cells.length / 2 - w / 2 + const y = cells.length / 2 - h / 2 - const crossOrigin = imageSettings.crossOrigin + const floorX = Math.floor(x) + const floorY = Math.floor(y) + const ceilW = Math.ceil(w + x - floorX) + const ceilH = Math.ceil(h + y - floorY) + const excavation = {x: floorX, y: floorY, w: ceilW, h: ceilH} - return {x, y, h, w, excavation, opacity, crossOrigin} + return {x, y, h, w, excavation} } function getMarginSize(includeMargin: boolean, marginSize?: number): number { @@ -230,7 +166,7 @@ function useQRCode({ minVersion, includeMargin, marginSize, - imageSettings, + logoSize, size, }: { value: string @@ -238,7 +174,7 @@ function useQRCode({ minVersion: number includeMargin: boolean marginSize?: number - imageSettings?: ImageSettings + logoSize?: number size: number }) { const qrcode = useMemo(() => { @@ -251,14 +187,14 @@ function useQRCode({ const margin = getMarginSize(includeMargin, marginSize) const numCells = cells.length + margin * 2 - const calculatedImageSettings = getImageSettings(cells, size, margin, imageSettings) + const calculatedImageSettings = getImageSettings(cells, size, margin, logoSize) return { cells, margin, numCells, calculatedImageSettings, } - }, [qrcode, size, imageSettings, includeMargin, marginSize]) + }, [qrcode, size, logoSize, includeMargin, marginSize]) return { qrcode, @@ -274,54 +210,30 @@ function QRCodeSVGComponent(props: QRProps) { value, size = DEFAULT_SIZE, level = DEFAULT_LEVEL, - fgColor = DEFAULT_FGCOLOR, + color: color = DEFAULT_FGCOLOR, minVersion = DEFAULT_MINVERSION, title, - imageExcavate, - imageSize, + logoSize, } = props const marginSize: number | undefined = undefined - const imageSettings = useMemo( - () => ({ - src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==', - height: imageSize, - width: imageSize, - excavate: imageExcavate, - }), - [imageExcavate, imageSize], - ) const {margin, cells, numCells, calculatedImageSettings} = useQRCode({ value, level, minVersion, includeMargin: DEFAULT_INCLUDEMARGIN, marginSize, - imageSettings, + logoSize, size, }) - let cellsToDraw = cells - let image = null - if (imageSettings != null && calculatedImageSettings != null) { - if (calculatedImageSettings.excavation != null) { - cellsToDraw = excavateModules(cells, calculatedImageSettings.excavation) - } - - image = ( - <image - href={imageSettings.src} - height={calculatedImageSettings.h} - width={calculatedImageSettings.w} - x={calculatedImageSettings.x + margin} - y={calculatedImageSettings.y + margin} - preserveAspectRatio="none" - opacity={calculatedImageSettings.opacity} - // Note: specified here always, but undefined will result in no attribute. - crossOrigin={calculatedImageSettings.crossOrigin} - /> - ) - } + const cellsToDraw = useMemo( + () => + logoSize && calculatedImageSettings?.excavation + ? excavateModules(cells, calculatedImageSettings.excavation) + : cells, + [calculatedImageSettings?.excavation, cells, logoSize], + ) // Drawing strategy: instead of a rect per module, we're going to create a // single path for the dark modules and layer that on top of a light rect, @@ -335,14 +247,13 @@ function QRCodeSVGComponent(props: QRProps) { <svg height={size} width={size} viewBox={`0 0 ${numCells} ${numCells}`} role="img"> {!!title && <title>{title}} - {image} ) } diff --git a/packages/presentation/src/preview/SharePreviewMenu.tsx b/packages/presentation/src/preview/SharePreviewMenu.tsx index 503297fb4..bea5cfcb7 100644 --- a/packages/presentation/src/preview/SharePreviewMenu.tsx +++ b/packages/presentation/src/preview/SharePreviewMenu.tsx @@ -34,9 +34,7 @@ import type {PreviewFrameProps} from './PreviewFrame' const QRCodeSVG = lazy(() => import('./QRCodeSVG')) export interface SharePreviewMenuProps { - // @TODO: Who can toggle sharing, need higher rights canToggleSharePreviewAccess: boolean - // @TODO: Who can use a shared preview access, if already enabled. If no rights, show a message instructing an admin to enable it. canUseSharedPreviewAccess: boolean previewLocationRoute: string initialUrl: PreviewFrameProps['initialUrl'] @@ -90,22 +88,13 @@ export const SharePreviewMenu = memo(function SharePreviewMenuComponent( throw error } - // const handleClose = useCallback(() => { - // if (busy) { - // // eslint-disable-next-line no-console - // console.warn('Show a toast here instead, and delay closing the dialog') - // } else { - // onClose() - // } - // }, [busy, onClose]) - const handleUnableToToggle = useCallback(() => { pushToast({ closable: true, status: 'warning', - title: `You don't have permissions to toggle sharing of this preview`, + title: t('share-preview-menu.error_toggle-sharing', {context: 'toggle-sharing'}), }) - }, [pushToast]) + }, [pushToast, t]) const handleDisableSharing = useCallback(async () => { try { @@ -227,7 +216,13 @@ export const SharePreviewMenu = memo(function SharePreviewMenuComponent( > {url ? 'Disable sharing' : 'Enable sharing'}} + content={ + + {t('share-preview-menu.toggle-button.tooltip', { + context: url ? 'disable' : 'enable', + })} + + } fallbackPlacements={['bottom-start']} padding={1} placement="bottom" @@ -247,11 +242,11 @@ export const SharePreviewMenu = memo(function SharePreviewMenuComponent( /> - Share this preview + {t('share-preview-menu.toggle-button.label', {context: 'first-line'})} - with anyone who has the link + {t('share-preview-menu.toggle-button.label', {context: 'second-line'})} @@ -279,11 +274,11 @@ export const SharePreviewMenu = memo(function SharePreviewMenuComponent( <> }> - QR code will appear here + {t('share-preview-menu.qr-code.placeholder')} )} - Scan the QR Code to open the preview on your phone. + {t('share-preview-menu.qr-code.instructions')} @@ -318,13 +313,13 @@ export const SharePreviewMenu = memo(function SharePreviewMenuComponent( onClick={handleCopyUrl} fontSize={1} padding={3} - text={'Copy preview link'} + text={t('share-preview-menu.copy-url.text')} /> ) : ( - You don't have permission to share previews. + {t('share-preview-menu.error', {context: 'missing-grants'})} )}