feat: option to save qr codes with no colors by default

pull/3083/head
William Grant 11 months ago
parent 5df14e74ff
commit d1a06a93de

@ -2,8 +2,9 @@ import { MouseEvent, useEffect, useRef, useState } from 'react';
import { QRCode } from 'react-qrcode-logo'; import { QRCode } from 'react-qrcode-logo';
import styled, { CSSProperties } from 'styled-components'; import styled, { CSSProperties } from 'styled-components';
import { THEME_GLOBALS } from '../themes/globals'; import { THEME_GLOBALS } from '../themes/globals';
import { saveQRCode } from '../util/saveQRCode'; import { saveBWQRCode } from '../util/saveBWQRCode';
import { AnimatedFlex } from './basic/Flex'; import { AnimatedFlex } from './basic/Flex';
import { SessionIconType } from './icon';
// AnimatedFlex because we fade in the QR code a flicker on first render // AnimatedFlex because we fade in the QR code a flicker on first render
const StyledQRView = styled(AnimatedFlex)<{ const StyledQRView = styled(AnimatedFlex)<{
@ -15,19 +16,22 @@ const StyledQRView = styled(AnimatedFlex)<{
${props => props.size && `width: ${props.size}px; height: ${props.size}px;`} ${props => props.size && `width: ${props.size}px; height: ${props.size}px;`}
`; `;
export type QRCodeLogoProps = { iconType: SessionIconType; iconSize: number };
export type SessionQRCodeProps = { export type SessionQRCodeProps = {
id: string; id: string;
value: string; value: string;
size: number; size: number;
hasLogo: boolean; hasLogo?: QRCodeLogoProps;
backgroundColor?: string; backgroundColor?: string;
foregroundColor?: string; foregroundColor?: string;
logoImage?: string; logoImage?: string;
logoSize?: number; logoSize?: number;
loading: boolean;
ariaLabel?: string; ariaLabel?: string;
dataTestId?: string; dataTestId?: string;
style?: CSSProperties; style?: CSSProperties;
loading?: boolean;
saveWithTheme?: boolean;
}; };
export function SessionQRCode(props: SessionQRCodeProps) { export function SessionQRCode(props: SessionQRCodeProps) {
@ -42,8 +46,9 @@ export function SessionQRCode(props: SessionQRCodeProps) {
logoSize, logoSize,
ariaLabel, ariaLabel,
dataTestId, dataTestId,
loading,
style, style,
loading,
saveWithTheme = false,
} = props; } = props;
const [logo, setLogo] = useState(logoImage); const [logo, setLogo] = useState(logoImage);
const [bgColor, setBgColor] = useState(backgroundColor); const [bgColor, setBgColor] = useState(backgroundColor);
@ -53,6 +58,26 @@ export function SessionQRCode(props: SessionQRCodeProps) {
const qrCanvasSize = 1000; const qrCanvasSize = 1000;
const canvasLogoSize = logoSize ? (qrCanvasSize * 0.25 * logoSize) / logoSize : 250; 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(() => { 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 // 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) { if (loading) {
@ -83,11 +108,7 @@ export function SessionQRCode(props: SessionQRCodeProps) {
title={window.i18n('clickToTrustContact')} title={window.i18n('clickToTrustContact')}
onClick={(event: MouseEvent<HTMLDivElement>) => { onClick={(event: MouseEvent<HTMLDivElement>) => {
event.preventDefault(); event.preventDefault();
void saveQRCode(id, { void saveQRCode();
...props,
id: `temp-${props.id}`,
style: { display: 'none' },
});
}} }}
data-testId={dataTestId || 'session-qr-code'} data-testId={dataTestId || 'session-qr-code'}
initial={{ opacity: 0 }} initial={{ opacity: 0 }}

@ -1,25 +1,27 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { useIconToImageURL } from '../../../hooks/useIconToImageURL'; import { useIconToImageURL } from '../../../hooks/useIconToImageURL';
import { SessionQRCode } from '../../SessionQRCode'; import { QRCodeLogoProps, SessionQRCode } from '../../SessionQRCode';
import { Avatar, AvatarSize } from '../../avatar/Avatar'; import { Avatar, AvatarSize } from '../../avatar/Avatar';
import { Flex } from '../../basic/Flex'; import { Flex } from '../../basic/Flex';
import { SpacerSM } from '../../basic/Text'; import { SpacerSM } from '../../basic/Text';
import { SessionIconButton } from '../../icon'; import { SessionIconButton } from '../../icon';
const qrLogoProps: QRCodeLogoProps = {
iconType: 'brand',
iconSize: 40,
};
export const QRView = ({ sessionID }: { sessionID: string }) => { export const QRView = ({ sessionID }: { sessionID: string }) => {
const { dataURL, iconSize, iconColor, backgroundColor, loading } = useIconToImageURL({ const { dataURL, iconSize, iconColor, backgroundColor, loading } = useIconToImageURL(qrLogoProps);
iconType: 'brand',
iconSize: 40,
});
return ( return (
<SessionQRCode <SessionQRCode
id={'session-account-id'} id={'session-account-id'}
value={sessionID} value={sessionID}
size={190} size={190}
hasLogo={true}
backgroundColor={backgroundColor} backgroundColor={backgroundColor}
foregroundColor={iconColor} foregroundColor={iconColor}
hasLogo={qrLogoProps}
logoImage={dataURL} logoImage={dataURL}
logoSize={iconSize} logoSize={iconSize}
loading={loading} loading={loading}

@ -12,7 +12,7 @@ import { useHideRecoveryPasswordEnabled } from '../../../state/selectors/setting
import { useIsDarkTheme } from '../../../state/selectors/theme'; import { useIsDarkTheme } from '../../../state/selectors/theme';
import { THEME_GLOBALS } from '../../../themes/globals'; import { THEME_GLOBALS } from '../../../themes/globals';
import { getCurrentRecoveryPhrase } from '../../../util/storage'; import { getCurrentRecoveryPhrase } from '../../../util/storage';
import { SessionQRCode } from '../../SessionQRCode'; import { QRCodeLogoProps, SessionQRCode } from '../../SessionQRCode';
import { AnimatedFlex } from '../../basic/Flex'; import { AnimatedFlex } from '../../basic/Flex';
import { SessionButtonColor } from '../../basic/SessionButton'; import { SessionButtonColor } from '../../basic/SessionButton';
import { SessionHtmlRenderer } from '../../basic/SessionHTMLRenderer'; import { SessionHtmlRenderer } from '../../basic/SessionHTMLRenderer';
@ -46,6 +46,11 @@ const StyledRecoveryPassword = styled(AnimatedFlex)<{ color: string }>`
color: ${props => props.color}; color: ${props => props.color};
`; `;
const qrLogoProps: QRCodeLogoProps = {
iconType: 'shield',
iconSize: 56,
};
export const SettingsCategoryRecoveryPassword = () => { export const SettingsCategoryRecoveryPassword = () => {
const [loadingSeed, setLoadingSeed] = useState(true); const [loadingSeed, setLoadingSeed] = useState(true);
const [recoveryPhrase, setRecoveryPhrase] = useState(''); const [recoveryPhrase, setRecoveryPhrase] = useState('');
@ -55,10 +60,7 @@ export const SettingsCategoryRecoveryPassword = () => {
const hideRecoveryPassword = useHideRecoveryPasswordEnabled(); const hideRecoveryPassword = useHideRecoveryPasswordEnabled();
const isDarkTheme = useIsDarkTheme(); const isDarkTheme = useIsDarkTheme();
const { dataURL, iconSize, iconColor, backgroundColor, loading } = useIconToImageURL({ const { dataURL, iconSize, iconColor, backgroundColor, loading } = useIconToImageURL(qrLogoProps);
iconType: 'shield',
iconSize: 56,
});
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -108,9 +110,9 @@ export const SettingsCategoryRecoveryPassword = () => {
id={'session-recovery-password'} id={'session-recovery-password'}
value={hexEncodedSeed} value={hexEncodedSeed}
size={260} size={260}
hasLogo={true}
backgroundColor={backgroundColor} backgroundColor={backgroundColor}
foregroundColor={iconColor} foregroundColor={iconColor}
hasLogo={qrLogoProps}
logoImage={dataURL} logoImage={dataURL}
logoSize={iconSize} logoSize={iconSize}
loading={loading} loading={loading}

@ -7,47 +7,45 @@ import { useIsDarkTheme } from '../state/selectors/theme';
import { ThemeKeys, getThemeValue } from '../themes/globals'; import { ThemeKeys, getThemeValue } from '../themes/globals';
const chooseIconColors = ( const chooseIconColors = (
isThemed: boolean, defaultColor: ThemeKeys,
isDarkTheme: boolean, darkColor?: ThemeKeys,
darkColor: ThemeKeys, lightColor?: ThemeKeys,
lightColor: ThemeKeys, isThemed?: boolean,
fallbackColor: ThemeKeys isDarkTheme?: boolean
) => { ) => {
return getThemeValue(isThemed ? (isDarkTheme ? darkColor : lightColor) : fallbackColor); return getThemeValue(
isThemed && darkColor && lightColor ? (isDarkTheme ? darkColor : lightColor) : defaultColor
);
}; };
const convertIconToImageURL = async ( export const convertIconToImageURL = async (
props: { isThemed: boolean; isDarkTheme: boolean } & Pick< props: Pick<SessionIconProps, 'iconType' | 'iconSize'> & {
SessionIconProps, isThemed?: boolean;
'iconType' | 'iconSize' | 'iconColor' | 'backgroundColor' isDarkTheme?: boolean;
> }
): Promise<{ dataUrl: string; bgColor: string; fgColor: string }> => { ): Promise<{ dataUrl: string; bgColor: string; fgColor: string }> => {
const { isThemed, isDarkTheme, iconType, iconSize } = props; 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) { const fgColor = chooseIconColors(
iconColor = chooseIconColors( '--black-color',
isThemed, '--background-primary-color',
isDarkTheme, '--text-primary-color',
'--background-primary-color', isThemed,
'--text-primary-color', isDarkTheme
'--black-color' );
);
} const bgColor = chooseIconColors(
'--white-color',
'--text-primary-color',
'--background-primary-color',
isThemed,
isDarkTheme
);
const root = document.querySelector('#root'); const root = document.querySelector('#root');
const divElement = document.createElement('div'); const divElement = document.createElement('div');
divElement.id = 'icon-to-image-url'; divElement.id = 'icon-to-image-url';
divElement.style.display = 'none';
root?.appendChild(divElement); root?.appendChild(divElement);
const reactRoot = createRoot(divElement!); const reactRoot = createRoot(divElement!);
@ -55,21 +53,23 @@ const convertIconToImageURL = async (
<SessionIcon <SessionIcon
iconType={iconType} iconType={iconType}
iconSize={iconSize} iconSize={iconSize}
iconColor={iconColor} iconColor={fgColor}
backgroundColor={backgroundColor} backgroundColor={bgColor}
/> />
); );
// wait for it to render // wait for it to render
await sleepFor(100); await sleepFor(100);
const svg = root?.querySelector(`#icon-to-image-url svg`); const svg = root?.querySelector(`#icon-to-image-url svg`);
svg?.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); svg?.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
const svgString = svg?.outerHTML; const svgString = svg?.outerHTML;
reactRoot?.unmount(); reactRoot?.unmount();
root?.removeChild(divElement); root?.removeChild(divElement);
return { return {
bgColor: backgroundColor, bgColor,
fgColor: iconColor, fgColor,
dataUrl: svgString ? `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}` : '', dataUrl: svgString ? `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}` : '',
}; };
}; };
@ -101,18 +101,17 @@ export const useIconToImageURL = ({
bgColor, bgColor,
fgColor, fgColor,
} = await convertIconToImageURL({ } = await convertIconToImageURL({
isThemed,
isDarkTheme,
iconType, iconType,
iconSize, iconSize,
iconColor, isThemed,
backgroundColor, isDarkTheme,
}); });
if (!newURL) { if (!newURL) {
throw new Error('[useIconToImageURL] Failed to convert icon to URL'); throw new Error('[useIconToImageURL] Failed to convert icon to URL');
} }
setInDarkTheme(isDarkTheme);
setBackgroundColor(bgColor); setBackgroundColor(bgColor);
setIconColor(fgColor); setIconColor(fgColor);
setDataURL(newURL); setDataURL(newURL);
@ -121,11 +120,10 @@ export const useIconToImageURL = ({
setMounted(true); setMounted(true);
} }
setLoading(false); setLoading(false);
setInDarkTheme(!!isThemed && isDarkTheme);
} catch (error) { } catch (error) {
window.log.error('[useIconToImageURL] Error fetching icon data url', error); window.log.error('[useIconToImageURL] Error fetching icon data url', error);
} }
}, [backgroundColor, iconColor, iconSize, iconType, isDarkTheme, isThemed, mounted]); }, [iconSize, iconType, isDarkTheme, isThemed, mounted]);
useMount(() => { useMount(() => {
void loadURL(); void loadURL();

@ -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<void> {
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(
<SessionQRCode
id={props.id}
value={props.value}
size={props.size}
hasLogo={props.hasLogo}
logoImage={logoImage}
logoSize={props.logoSize}
/>
);
// 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);
}
}

@ -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<void> {
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(<SessionQRCode {...customProps} />);
// 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!');
}
}
Loading…
Cancel
Save