From d1a06a93de0fc9f780fd6f7b2eb6c8f8a0ffc37c Mon Sep 17 00:00:00 2001 From: William Grant Date: Wed, 12 Jun 2024 13:53:29 +1000 Subject: [PATCH] feat: option to save qr codes with no colors by default --- ts/components/SessionQRCode.tsx | 39 ++++++--- .../dialog/edit-profile/components.tsx | 14 ++-- .../section/CategoryRecoveryPassword.tsx | 14 ++-- ts/hooks/useIconToImageURL.tsx | 80 +++++++++---------- ts/util/saveBWQRCode.tsx | 54 +++++++++++++ ts/util/saveQRCode.tsx | 34 -------- 6 files changed, 139 insertions(+), 96 deletions(-) create mode 100644 ts/util/saveBWQRCode.tsx delete mode 100644 ts/util/saveQRCode.tsx diff --git a/ts/components/SessionQRCode.tsx b/ts/components/SessionQRCode.tsx index f7db9e5c4..b64bf7447 100644 --- a/ts/components/SessionQRCode.tsx +++ b/ts/components/SessionQRCode.tsx @@ -2,8 +2,9 @@ import { MouseEvent, useEffect, useRef, useState } from 'react'; import { QRCode } from 'react-qrcode-logo'; import styled, { CSSProperties } from 'styled-components'; import { THEME_GLOBALS } from '../themes/globals'; -import { saveQRCode } from '../util/saveQRCode'; +import { saveBWQRCode } from '../util/saveBWQRCode'; import { AnimatedFlex } from './basic/Flex'; +import { SessionIconType } from './icon'; // AnimatedFlex because we fade in the QR code a flicker on first render const StyledQRView = styled(AnimatedFlex)<{ @@ -15,19 +16,22 @@ const StyledQRView = styled(AnimatedFlex)<{ ${props => props.size && `width: ${props.size}px; height: ${props.size}px;`} `; +export type QRCodeLogoProps = { iconType: SessionIconType; iconSize: number }; + export type SessionQRCodeProps = { id: string; value: string; size: number; - hasLogo: boolean; + hasLogo?: QRCodeLogoProps; backgroundColor?: string; foregroundColor?: string; logoImage?: string; logoSize?: number; - loading: boolean; ariaLabel?: string; dataTestId?: string; style?: CSSProperties; + loading?: boolean; + saveWithTheme?: boolean; }; export function SessionQRCode(props: SessionQRCodeProps) { @@ -42,8 +46,9 @@ export function SessionQRCode(props: SessionQRCodeProps) { logoSize, ariaLabel, dataTestId, - loading, style, + loading, + saveWithTheme = false, } = props; const [logo, setLogo] = useState(logoImage); const [bgColor, setBgColor] = useState(backgroundColor); @@ -53,6 +58,26 @@ export function SessionQRCode(props: SessionQRCodeProps) { const qrCanvasSize = 1000; const canvasLogoSize = logoSize ? (qrCanvasSize * 0.25 * logoSize) / logoSize : 250; + const saveQRCode = async () => { + const fileName = `${id}-${new Date().toISOString()}.jpg`; + try { + if (saveWithTheme) { + qrRef.current?.download('jpg', fileName); + } else { + void saveBWQRCode(fileName, { + id: `${id}-save`, + value, + size, + hasLogo, + logoImage, + logoSize, + }); + } + } catch (err) { + window.log.error(`QR code save failed! ${fileName}\n${err}`); + } + }; + useEffect(() => { // Don't pass the component props to the QR component directly instead update it's props in the next render cycle to prevent janky renders if (loading) { @@ -83,11 +108,7 @@ export function SessionQRCode(props: SessionQRCodeProps) { title={window.i18n('clickToTrustContact')} onClick={(event: MouseEvent) => { event.preventDefault(); - void saveQRCode(id, { - ...props, - id: `temp-${props.id}`, - style: { display: 'none' }, - }); + void saveQRCode(); }} data-testId={dataTestId || 'session-qr-code'} initial={{ opacity: 0 }} diff --git a/ts/components/dialog/edit-profile/components.tsx b/ts/components/dialog/edit-profile/components.tsx index 80e7904e1..47754acf9 100644 --- a/ts/components/dialog/edit-profile/components.tsx +++ b/ts/components/dialog/edit-profile/components.tsx @@ -1,25 +1,27 @@ import styled from 'styled-components'; import { useIconToImageURL } from '../../../hooks/useIconToImageURL'; -import { SessionQRCode } from '../../SessionQRCode'; +import { QRCodeLogoProps, SessionQRCode } from '../../SessionQRCode'; import { Avatar, AvatarSize } from '../../avatar/Avatar'; import { Flex } from '../../basic/Flex'; import { SpacerSM } from '../../basic/Text'; import { SessionIconButton } from '../../icon'; +const qrLogoProps: QRCodeLogoProps = { + iconType: 'brand', + iconSize: 40, +}; + export const QRView = ({ sessionID }: { sessionID: string }) => { - const { dataURL, iconSize, iconColor, backgroundColor, loading } = useIconToImageURL({ - iconType: 'brand', - iconSize: 40, - }); + const { dataURL, iconSize, iconColor, backgroundColor, loading } = useIconToImageURL(qrLogoProps); return ( ` color: ${props => props.color}; `; +const qrLogoProps: QRCodeLogoProps = { + iconType: 'shield', + iconSize: 56, +}; + export const SettingsCategoryRecoveryPassword = () => { const [loadingSeed, setLoadingSeed] = useState(true); const [recoveryPhrase, setRecoveryPhrase] = useState(''); @@ -55,10 +60,7 @@ export const SettingsCategoryRecoveryPassword = () => { const hideRecoveryPassword = useHideRecoveryPasswordEnabled(); const isDarkTheme = useIsDarkTheme(); - const { dataURL, iconSize, iconColor, backgroundColor, loading } = useIconToImageURL({ - iconType: 'shield', - iconSize: 56, - }); + const { dataURL, iconSize, iconColor, backgroundColor, loading } = useIconToImageURL(qrLogoProps); const dispatch = useDispatch(); @@ -108,9 +110,9 @@ export const SettingsCategoryRecoveryPassword = () => { id={'session-recovery-password'} value={hexEncodedSeed} size={260} - hasLogo={true} backgroundColor={backgroundColor} foregroundColor={iconColor} + hasLogo={qrLogoProps} logoImage={dataURL} logoSize={iconSize} loading={loading} diff --git a/ts/hooks/useIconToImageURL.tsx b/ts/hooks/useIconToImageURL.tsx index d0daa0400..27fc3144d 100644 --- a/ts/hooks/useIconToImageURL.tsx +++ b/ts/hooks/useIconToImageURL.tsx @@ -7,47 +7,45 @@ import { useIsDarkTheme } from '../state/selectors/theme'; import { ThemeKeys, getThemeValue } from '../themes/globals'; const chooseIconColors = ( - isThemed: boolean, - isDarkTheme: boolean, - darkColor: ThemeKeys, - lightColor: ThemeKeys, - fallbackColor: ThemeKeys + defaultColor: ThemeKeys, + darkColor?: ThemeKeys, + lightColor?: ThemeKeys, + isThemed?: boolean, + isDarkTheme?: boolean ) => { - return getThemeValue(isThemed ? (isDarkTheme ? darkColor : lightColor) : fallbackColor); + return getThemeValue( + isThemed && darkColor && lightColor ? (isDarkTheme ? darkColor : lightColor) : defaultColor + ); }; -const convertIconToImageURL = async ( - props: { isThemed: boolean; isDarkTheme: boolean } & Pick< - SessionIconProps, - 'iconType' | 'iconSize' | 'iconColor' | 'backgroundColor' - > +export const convertIconToImageURL = async ( + props: Pick & { + isThemed?: boolean; + isDarkTheme?: boolean; + } ): Promise<{ dataUrl: string; bgColor: string; fgColor: string }> => { const { isThemed, isDarkTheme, iconType, iconSize } = props; - let { iconColor, backgroundColor } = props; - - if (!backgroundColor) { - backgroundColor = chooseIconColors( - isThemed, - isDarkTheme, - '--text-primary-color', - '--background-primary-color', - '--white-color' - ); - } - if (!iconColor) { - iconColor = chooseIconColors( - isThemed, - isDarkTheme, - '--background-primary-color', - '--text-primary-color', - '--black-color' - ); - } + const fgColor = chooseIconColors( + '--black-color', + '--background-primary-color', + '--text-primary-color', + isThemed, + isDarkTheme + ); + + const bgColor = chooseIconColors( + '--white-color', + '--text-primary-color', + '--background-primary-color', + isThemed, + isDarkTheme + ); const root = document.querySelector('#root'); const divElement = document.createElement('div'); divElement.id = 'icon-to-image-url'; + divElement.style.display = 'none'; root?.appendChild(divElement); const reactRoot = createRoot(divElement!); @@ -55,21 +53,23 @@ const convertIconToImageURL = async ( ); // wait for it to render await sleepFor(100); + const svg = root?.querySelector(`#icon-to-image-url svg`); svg?.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); const svgString = svg?.outerHTML; + reactRoot?.unmount(); root?.removeChild(divElement); return { - bgColor: backgroundColor, - fgColor: iconColor, + bgColor, + fgColor, dataUrl: svgString ? `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}` : '', }; }; @@ -101,18 +101,17 @@ export const useIconToImageURL = ({ bgColor, fgColor, } = await convertIconToImageURL({ - isThemed, - isDarkTheme, iconType, iconSize, - iconColor, - backgroundColor, + isThemed, + isDarkTheme, }); if (!newURL) { throw new Error('[useIconToImageURL] Failed to convert icon to URL'); } + setInDarkTheme(isDarkTheme); setBackgroundColor(bgColor); setIconColor(fgColor); setDataURL(newURL); @@ -121,11 +120,10 @@ export const useIconToImageURL = ({ setMounted(true); } setLoading(false); - setInDarkTheme(!!isThemed && isDarkTheme); } catch (error) { window.log.error('[useIconToImageURL] Error fetching icon data url', error); } - }, [backgroundColor, iconColor, iconSize, iconType, isDarkTheme, isThemed, mounted]); + }, [iconSize, iconType, isDarkTheme, isThemed, mounted]); useMount(() => { void loadURL(); diff --git a/ts/util/saveBWQRCode.tsx b/ts/util/saveBWQRCode.tsx new file mode 100644 index 000000000..c7bf5c250 --- /dev/null +++ b/ts/util/saveBWQRCode.tsx @@ -0,0 +1,54 @@ +import { createRoot } from 'react-dom/client'; +import { SessionQRCode, SessionQRCodeProps } from '../components/SessionQRCode'; +import { convertIconToImageURL } from '../hooks/useIconToImageURL'; +import { sleepFor } from '../session/utils/Promise'; +import { saveURLAsFile } from './saveURLAsFile'; + +export async function saveBWQRCode(filename: string, props: SessionQRCodeProps): Promise { + try { + const root = document.querySelector('#root'); + const divElement = document.createElement('div'); + divElement.style.display = 'none'; + root?.appendChild(divElement); + + let logoImage = props.logoImage; + + if (props.hasLogo) { + const { dataUrl } = await convertIconToImageURL(props.hasLogo); + logoImage = dataUrl; + } + + const reactRoot = createRoot(divElement!); + reactRoot.render( + + ); + // wait for it to render + await sleepFor(100); + + const qrCanvas = root?.querySelector(`#${props.id}-canvas`); + if (qrCanvas) { + const url = (qrCanvas as HTMLCanvasElement).toDataURL('image/jpeg'); + if (url) { + saveURLAsFile({ + filename, + url, + document, + }); + } + } else { + throw Error('QR Code canvas not found'); + } + + reactRoot?.unmount(); + root?.removeChild(divElement); + } catch (err) { + window.log.error('WIP: [saveBWQRCode] failed', err); + } +} diff --git a/ts/util/saveQRCode.tsx b/ts/util/saveQRCode.tsx deleted file mode 100644 index b99e922f6..000000000 --- a/ts/util/saveQRCode.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { isEmpty } from 'lodash'; -import { createRoot } from 'react-dom/client'; -import { SessionQRCode, SessionQRCodeProps } from '../components/SessionQRCode'; -import { sleepFor } from '../session/utils/Promise'; -import { saveURLAsFile } from './saveURLAsFile'; - -export async function saveQRCode(id: string, customProps?: SessionQRCodeProps): Promise { - let qrCanvas: HTMLCanvasElement | undefined; - - if (!isEmpty(customProps)) { - const root = document.querySelector('#root'); - const divElement = document.createElement('div'); - root?.appendChild(divElement); - const reactRoot = createRoot(divElement!); - reactRoot.render(); - // wait for it to render - await sleepFor(100); - qrCanvas = root?.querySelector(`#${customProps.id}`) as HTMLCanvasElement; - reactRoot?.unmount(); - root?.removeChild(divElement); - } else { - qrCanvas = document.querySelector(`#${id}`) as HTMLCanvasElement; - } - - if (qrCanvas) { - saveURLAsFile({ - filename: `${id}-${new Date().toISOString()}.png`, - url: qrCanvas.toDataURL(), - document, - }); - } else { - window.log.error('[saveQRCode] QR code not found!'); - } -}