feat: click on qr codes now opens the lightbox

lightbox now supports saving data blobs
pull/3083/head
William Grant 9 months ago
parent 009795eb3b
commit 490e4957f4

@ -2,7 +2,7 @@ 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 { saveBWQRCode } from '../util/saveBWQRCode';
import { renderQRCode } from '../util/qrCodes';
import { AnimatedFlex } from './basic/Flex';
import { SessionIconType } from './icon';
@ -27,11 +27,11 @@ export type SessionQRCodeProps = {
hasLogo?: QRCodeLogoProps;
logoImage?: string;
logoSize?: number;
loading?: boolean;
onClick?: (fileName: string, dataUrl: string) => void;
ariaLabel?: string;
dataTestId?: string;
style?: CSSProperties;
loading?: boolean;
saveWithTheme?: boolean;
};
export function SessionQRCode(props: SessionQRCodeProps) {
@ -44,11 +44,11 @@ export function SessionQRCode(props: SessionQRCodeProps) {
hasLogo,
logoImage,
logoSize,
loading,
onClick,
ariaLabel,
dataTestId,
style,
loading,
saveWithTheme = false,
} = props;
const [logo, setLogo] = useState(logoImage);
const [bgColor, setBgColor] = useState(backgroundColor);
@ -58,24 +58,33 @@ export function SessionQRCode(props: SessionQRCodeProps) {
const qrCanvasSize = 1000;
const canvasLogoSize = logoSize ? (qrCanvasSize * 0.25 * logoSize) / logoSize : 250;
const saveQRCode = () => {
const loadQRCodeDataUrl = async () => {
const fileName = `${id}-${new Date().toISOString()}.jpg`;
let url = '';
try {
if (saveWithTheme) {
qrRef.current?.download('jpg', fileName);
} else {
void saveBWQRCode(fileName, {
url = await renderQRCode(
{
id: `${id}-save`,
value,
size,
hasLogo,
logoImage,
logoSize,
});
}
},
fileName
);
} catch (err) {
window.log.error(`QR code save failed! ${fileName}\n${err}`);
}
return { fileName, url };
};
const handleOnClick = async () => {
const { fileName, url } = await loadQRCodeDataUrl();
if (onClick) {
onClick(fileName, url);
}
};
useEffect(() => {
@ -108,7 +117,7 @@ export function SessionQRCode(props: SessionQRCodeProps) {
title={window.i18n('clickToTrustContact')}
onClick={(event: MouseEvent<HTMLDivElement>) => {
event.preventDefault();
void saveQRCode();
void handleOnClick();
}}
data-testId={dataTestId || 'session-qr-code'}
initial={{ opacity: 0 }}

@ -103,9 +103,10 @@ const handleKeyEscape = (
loading: boolean,
dispatch: Dispatch
) => {
if (loading) {
if (loading || mode === 'lightbox') {
return;
}
if (mode === 'edit') {
setMode('default');
setProfileNameError(undefined);
@ -179,7 +180,7 @@ const updateDisplayName = async (newName: string) => {
await SyncUtils.forceSyncConfigurationNowIfNeeded(true);
};
export type ProfileDialogModes = 'default' | 'edit' | 'qr';
export type ProfileDialogModes = 'default' | 'edit' | 'qr' | 'lightbox';
export const EditProfileDialog = () => {
const dispatch = useDispatch();
@ -289,7 +290,7 @@ export const EditProfileDialog = () => {
additionalClassName={mode === 'default' ? 'edit-profile-default' : undefined}
>
{mode === 'qr' ? (
<QRView sessionID={ourId} />
<QRView sessionID={ourId} setMode={setMode} />
) : (
<>
<SpacerXL />

@ -1,17 +1,26 @@
import styled from 'styled-components';
import { useIconToImageURL } from '../../../hooks/useIconToImageURL';
import { updateLightBoxOptions } from '../../../state/ducks/modalDialog';
import { prepareQRCodeForLightBox } from '../../../util/qrCodes';
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';
import { ProfileDialogModes } from './EditProfileDialog';
const qrLogoProps: QRCodeLogoProps = {
iconType: 'brandThin',
iconSize: 42,
};
export const QRView = ({ sessionID }: { sessionID: string }) => {
export const QRView = ({
sessionID,
setMode,
}: {
sessionID: string;
setMode: (mode: ProfileDialogModes) => void;
}) => {
const { dataURL, iconSize, iconColor, backgroundColor, loading } = useIconToImageURL(qrLogoProps);
return (
@ -25,6 +34,13 @@ export const QRView = ({ sessionID }: { sessionID: string }) => {
logoImage={dataURL}
logoSize={iconSize}
loading={loading}
onClick={(fileName, dataUrl) => {
const lightBoxOptions = prepareQRCodeForLightBox(fileName, dataUrl, () => {
setMode('edit');
});
window.inboxStore?.dispatch(updateLightBoxOptions(lightBoxOptions));
setMode('lightbox');
}}
ariaLabel={'Account ID QR code'}
dataTestId={'your-qr-code'}
style={{ marginTop: '-1px' }}

@ -41,7 +41,7 @@ const styles = {
width: '100vw',
height: '100vh',
left: 0,
zIndex: 5,
zIndex: 150, // modals are 100
right: 0,
top: 0,
bottom: 0,

@ -9,6 +9,7 @@ import { useSelectedConversationKey } from '../../state/selectors/selectedConver
import { MIME } from '../../types';
import { AttachmentTypeWithPath } from '../../types/Attachment';
import { saveAttachmentToDisk } from '../../util/attachmentsUtil';
import { saveURLAsFile } from '../../util/saveURLAsFile';
export interface MediaItemType {
objectURL?: string;
@ -24,10 +25,11 @@ export interface MediaItemType {
type Props = {
media: Array<MediaItemType>;
selectedIndex?: number;
onClose?: () => void;
};
export const LightboxGallery = (props: Props) => {
const { media, selectedIndex = -1 } = props;
const { media, selectedIndex = -1, onClose } = props;
const [currentIndex, setCurrentIndex] = useState(-1);
const selectedConversation = useSelectedConversationKey();
@ -40,6 +42,9 @@ export const LightboxGallery = (props: Props) => {
}, []);
const selectedMedia = media[currentIndex];
const objectURL = selectedMedia?.objectURL || 'images/alert-outline.svg';
const isDataBlob = objectURL.startsWith('data:');
const firstIndex = 0;
const lastIndex = media.length - 1;
@ -55,12 +60,22 @@ export const LightboxGallery = (props: Props) => {
}, [currentIndex, lastIndex]);
const handleSave = useCallback(() => {
const mediaItem = media[currentIndex];
if (isDataBlob && mediaItem.objectURL) {
saveURLAsFile({
filename: mediaItem.attachment.fileName,
url: mediaItem.objectURL,
document,
});
} else {
if (!selectedConversation) {
return;
}
const mediaItem = media[currentIndex];
void saveAttachmentToDisk({ ...mediaItem, conversationId: selectedConversation });
}, [currentIndex, media, selectedConversation]);
}
}, [currentIndex, isDataBlob, media, selectedConversation]);
useKey(
'ArrowRight',
@ -68,7 +83,7 @@ export const LightboxGallery = (props: Props) => {
onNext?.();
},
undefined,
[currentIndex]
[onNext, currentIndex]
);
useKey(
'ArrowLeft',
@ -76,18 +91,21 @@ export const LightboxGallery = (props: Props) => {
onPrevious?.();
},
undefined,
[currentIndex]
[onPrevious, currentIndex]
);
useKey(
'Escape',
() => {
dispatch(updateLightBoxOptions(null));
if (onClose) {
onClose();
}
},
undefined,
[currentIndex]
[currentIndex, updateLightBoxOptions, dispatch, onClose]
);
if (!selectedConversation) {
if (!isDataBlob && !selectedConversation) {
return null;
}
@ -95,7 +113,7 @@ export const LightboxGallery = (props: Props) => {
if (currentIndex === -1) {
return null;
}
const objectURL = selectedMedia?.objectURL || 'images/alert-outline.svg';
const { attachment } = selectedMedia;
const caption = attachment?.caption;

@ -6,11 +6,15 @@ import styled from 'styled-components';
import { useIconToImageURL } from '../../../hooks/useIconToImageURL';
import { usePasswordModal } from '../../../hooks/usePasswordModal';
import { mnDecode } from '../../../session/crypto/mnemonic';
import { updateHideRecoveryPasswordModel } from '../../../state/ducks/modalDialog';
import {
updateHideRecoveryPasswordModel,
updateLightBoxOptions,
} from '../../../state/ducks/modalDialog';
import { showSettingsSection } from '../../../state/ducks/section';
import { useHideRecoveryPasswordEnabled } from '../../../state/selectors/settings';
import { useIsDarkTheme } from '../../../state/selectors/theme';
import { THEME_GLOBALS } from '../../../themes/globals';
import { prepareQRCodeForLightBox } from '../../../util/qrCodes';
import { getCurrentRecoveryPhrase } from '../../../util/storage';
import { QRCodeLogoProps, SessionQRCode } from '../../SessionQRCode';
import { AnimatedFlex } from '../../basic/Flex';
@ -116,6 +120,10 @@ export const SettingsCategoryRecoveryPassword = () => {
logoImage={dataURL}
logoSize={iconSize}
loading={loading}
onClick={(fileName, dataUrl) => {
const lightBoxOptions = prepareQRCodeForLightBox(fileName, dataUrl);
window.inboxStore?.dispatch(updateLightBoxOptions(lightBoxOptions));
}}
ariaLabel={'Recovery Password QR Code'}
dataTestId={'session-recovery-password'}
/>

@ -46,6 +46,7 @@ export type LightBoxOptions = {
media: Array<MediaItemType>;
attachment: AttachmentTypeWithPath;
selectedIndex?: number;
onClose?: () => void;
} | null;
export type ModalState = {

@ -1,10 +1,43 @@
import { createRoot } from 'react-dom/client';
import { SessionQRCode, SessionQRCodeProps } from '../components/SessionQRCode';
import { convertIconToImageURL } from '../hooks/useIconToImageURL';
import { UserUtils } from '../session/utils';
import { sleepFor } from '../session/utils/Promise';
import { saveURLAsFile } from './saveURLAsFile';
import { LightBoxOptions } from '../state/ducks/modalDialog';
export function prepareQRCodeForLightBox(fileName: string, url: string, onClose?: () => void) {
const attachment = {
fileName,
url,
fileSize: '',
path: url,
id: 0,
contentType: 'image/jpeg',
screenshot: null,
thumbnail: null,
};
const lightBoxOptions: LightBoxOptions = {
media: [
{
index: 0,
objectURL: url,
contentType: 'image/jpeg',
attachment,
messageSender: UserUtils.getOurPubKeyStrFromCache(),
messageTimestamp: -1,
messageId: '',
},
],
attachment,
onClose,
};
return lightBoxOptions;
}
export async function renderQRCode(props: SessionQRCodeProps, filename: string): Promise<string> {
let url = '';
export async function saveBWQRCode(filename: string, props: SessionQRCodeProps): Promise<void> {
try {
const root = document.querySelector('#root');
const divElement = document.createElement('div');
@ -34,14 +67,7 @@ export async function saveBWQRCode(filename: string, props: SessionQRCodeProps):
const qrCanvas = root?.querySelector(`#${props.id}-canvas`);
if (qrCanvas) {
const url = (qrCanvas as HTMLCanvasElement).toDataURL('image/jpeg');
if (url) {
saveURLAsFile({
filename,
url,
document,
});
}
url = (qrCanvas as HTMLCanvasElement).toDataURL('image/jpeg');
} else {
throw Error('QR Code canvas not found');
}
@ -51,4 +77,6 @@ export async function saveBWQRCode(filename: string, props: SessionQRCodeProps):
} catch (err) {
window.log.error(`[saveBWQRCode] failed for ${filename}`, err);
}
return url;
}
Loading…
Cancel
Save