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

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

@ -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<HTMLDivElement>) => {
event.preventDefault();
void saveQRCode(id, {
...props,
id: `temp-${props.id}`,
style: { display: 'none' },
});
void saveQRCode();
}}
data-testId={dataTestId || 'session-qr-code'}
initial={{ opacity: 0 }}

@ -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 (
<SessionQRCode
id={'session-account-id'}
value={sessionID}
size={190}
hasLogo={true}
backgroundColor={backgroundColor}
foregroundColor={iconColor}
hasLogo={qrLogoProps}
logoImage={dataURL}
logoSize={iconSize}
loading={loading}

@ -12,7 +12,7 @@ import { useHideRecoveryPasswordEnabled } from '../../../state/selectors/setting
import { useIsDarkTheme } from '../../../state/selectors/theme';
import { THEME_GLOBALS } from '../../../themes/globals';
import { getCurrentRecoveryPhrase } from '../../../util/storage';
import { SessionQRCode } from '../../SessionQRCode';
import { QRCodeLogoProps, SessionQRCode } from '../../SessionQRCode';
import { AnimatedFlex } from '../../basic/Flex';
import { SessionButtonColor } from '../../basic/SessionButton';
import { SessionHtmlRenderer } from '../../basic/SessionHTMLRenderer';
@ -46,6 +46,11 @@ const StyledRecoveryPassword = styled(AnimatedFlex)<{ color: string }>`
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}

@ -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<SessionIconProps, 'iconType' | 'iconSize'> & {
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 (
<SessionIcon
iconType={iconType}
iconSize={iconSize}
iconColor={iconColor}
backgroundColor={backgroundColor}
iconColor={fgColor}
backgroundColor={bgColor}
/>
);
// 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();

@ -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