diff --git a/src/components/MessageDialog.tsx b/src/components/MessageDialog.tsx index b9d50ffe4..ab298591a 100644 --- a/src/components/MessageDialog.tsx +++ b/src/components/MessageDialog.tsx @@ -49,7 +49,7 @@ export default function MessageDialog({ {(children || attributes?.content) && ( {children || ( -

+

{attributes.content}

)} diff --git a/src/components/pages/gallery/Upload.tsx b/src/components/pages/gallery/Upload.tsx index 1dbba3050..77062826a 100644 --- a/src/components/pages/gallery/Upload.tsx +++ b/src/components/pages/gallery/Upload.tsx @@ -24,6 +24,8 @@ import { FileUploadResults, UPLOAD_STAGES } from 'constants/upload'; import { ElectronFile, FileWithCollection } from 'types/upload'; import UploadTypeChoiceModal from './UploadTypeChoiceModal'; import Router from 'next/router'; +import { isCanvasBlocked } from 'utils/upload/isCanvasBlocked'; +import { downloadApp } from 'utils/common'; const FIRST_ALBUM_NAME = 'My First Album'; @@ -47,6 +49,7 @@ interface Props { setElectronFiles: (files: ElectronFile[]) => void; showUploadTypeChoiceModal: boolean; setShowUploadTypeChoiceModal: (open: boolean) => void; + SetDialogMessage: SetDialogMessage; } enum UPLOAD_STRATEGY { @@ -121,32 +124,48 @@ export default function Upload(props: Props) { useEffect(() => { if ( - !props.uploadInProgress && - (props.electronFiles?.length > 0 || - props.droppedFiles?.length > 0 || - appContext.sharedFiles?.length > 0) + props.electronFiles?.length > 0 || + props.droppedFiles?.length > 0 || + appContext.sharedFiles?.length > 0 ) { - props.setLoading(true); - if (props.droppedFiles?.length > 0) { - // File selection by drag and drop or selection of file. - toUploadFiles.current = props.droppedFiles; - props.clearDroppedFiles(); - } else if (appContext.sharedFiles?.length > 0) { - toUploadFiles.current = appContext.sharedFiles; - appContext.resetSharedFiles(); - } else if (props.electronFiles?.length > 0) { - // File selection from desktop app - toUploadFiles.current = props.electronFiles; - props.setElectronFiles([]); - } - const analysisResult = analyseUploadFiles(); - setAnalysisResult(analysisResult); + if (props.uploadInProgress) { + // no-op + // a upload is already in progress + } else if (isCanvasBlocked()) { + props.setDialogMessage({ + title: constants.CANVAS_BLOCKED_TITLE, + staticBackdrop: true, + content: constants.CANVAS_BLOCKED_MESSAGE(), + close: { text: constants.CLOSE }, + proceed: { + text: constants.DOWNLOAD_APP, + action: downloadApp, + variant: 'success', + }, + }); + } else { + props.setLoading(true); + if (props.droppedFiles?.length > 0) { + // File selection by drag and drop or selection of file. + toUploadFiles.current = props.droppedFiles; + props.clearDroppedFiles(); + } else if (appContext.sharedFiles?.length > 0) { + toUploadFiles.current = appContext.sharedFiles; + appContext.resetSharedFiles(); + } else if (props.electronFiles?.length > 0) { + // File selection from desktop app + toUploadFiles.current = props.electronFiles; + props.setElectronFiles([]); + } + const analysisResult = analyseUploadFiles(); + setAnalysisResult(analysisResult); - handleCollectionCreationAndUpload( - analysisResult, - props.isFirstUpload - ); - props.setLoading(false); + handleCollectionCreationAndUpload( + analysisResult, + props.isFirstUpload + ); + props.setLoading(false); + } } }, [props.droppedFiles, appContext.sharedFiles, props.electronFiles]); diff --git a/src/components/pages/gallery/UploadProgress.tsx b/src/components/pages/gallery/UploadProgress.tsx index c1635eec2..4b920eded 100644 --- a/src/components/pages/gallery/UploadProgress.tsx +++ b/src/components/pages/gallery/UploadProgress.tsx @@ -5,7 +5,7 @@ import { Accordion, Button, Modal, ProgressBar } from 'react-bootstrap'; import { FileRejection } from 'react-dropzone'; import styled from 'styled-components'; -import { DESKTOP_APP_DOWNLOAD_URL } from 'utils/common'; +import { getOSSpecificDesktopAppDownloadLink } from 'utils/common'; import constants from 'utils/strings/constants'; import { ButtonVariant, getVariantColor } from './LinkButton'; import { FileUploadResults, UPLOAD_STAGES } from 'constants/upload'; @@ -292,7 +292,7 @@ export default function UploadProgress(props: Props) { fileUploadResult={FileUploadResults.BLOCKED} sectionTitle={constants.BLOCKED_UPLOADS} sectionInfo={constants.ETAGS_BLOCKED( - DESKTOP_APP_DOWNLOAD_URL + getOSSpecificDesktopAppDownloadLink() )} /> { } // iOS detection from: http://stackoverflow.com/a/9039885/177710 - if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) { + if (/(iPad|iPhone|iPod)/g.test(userAgent) && !window.MSStream) { return OS.IOS; } + // credit: https://github.com/MikeKovarik/platform-detect/blob/master/os.mjs + if (userAgent.includes('Windows')) { + return OS.WINDOWS; + } + if (userAgent.includes('Macintosh')) { + return OS.MAC; + } + // Linux must be last + if (userAgent.includes('Linux')) { + return OS.LINUX; + } + return OS.UNKNOWN; }; diff --git a/src/utils/common/index.ts b/src/utils/common/index.ts index 170f761b0..9f9f4503f 100644 --- a/src/utils/common/index.ts +++ b/src/utils/common/index.ts @@ -1,9 +1,12 @@ import constants from 'utils/strings/constants'; import { CustomError } from 'utils/error'; +import GetDeviceOS, { OS } from './deviceDetection'; -export const DESKTOP_APP_DOWNLOAD_URL = +const DESKTOP_APP_GITHUB_DOWNLOAD_URL = 'https://github.com/ente-io/bhari-frame/releases/latest'; +const APP_DOWNLOAD_ENTE_URL_PREFIX = 'https://ente.io/download'; + export function checkConnectivity() { if (navigator.onLine) { return true; @@ -21,8 +24,21 @@ export async function sleep(time: number) { }); } +export function getOSSpecificDesktopAppDownloadLink() { + const os = GetDeviceOS(); + let url = ''; + if (os === OS.WINDOWS) { + url = `${APP_DOWNLOAD_ENTE_URL_PREFIX}/exe`; + } else if (os === OS.MAC) { + url = `${APP_DOWNLOAD_ENTE_URL_PREFIX}/dmg`; + } else { + url = DESKTOP_APP_GITHUB_DOWNLOAD_URL; + } + return url; +} export function downloadApp() { - const win = window.open(DESKTOP_APP_DOWNLOAD_URL, '_blank'); + const link = getOSSpecificDesktopAppDownloadLink(); + const win = window.open(link, '_blank'); win.focus(); } diff --git a/src/utils/strings/englishConstants.tsx b/src/utils/strings/englishConstants.tsx index bff49d90c..22e79d461 100644 --- a/src/utils/strings/englishConstants.tsx +++ b/src/utils/strings/englishConstants.tsx @@ -713,6 +713,19 @@ const englishConstants = { 'are you sure that you want to stop all the uploads in progress?', STOP_UPLOADS_HEADER: 'stop uploads?', YES_STOP_UPLOADS: 'yes, stop uploads', + CANVAS_BLOCKED_TITLE: 'unable to generate thumbnail', + CANVAS_BLOCKED_MESSAGE: () => ( + <> +

+ it looks like your browser has disabled access to canvas, which + is necessary to generate thumbnails for your photos +

+

+ please enable access to your browser's canvas, or check out our + desktop app +

+ + ), }; export default englishConstants; diff --git a/src/utils/upload/isCanvasBlocked.ts b/src/utils/upload/isCanvasBlocked.ts new file mode 100644 index 000000000..d637b5de4 --- /dev/null +++ b/src/utils/upload/isCanvasBlocked.ts @@ -0,0 +1,53 @@ +// +// Canvas Blocker & +// Firefox privacy.resistFingerprinting Detector. +// (c) 2018 // JOHN OZBAY // CRYPT.EE +// MIT License + +// Credits: https://github.com/johnozbay/canvas-block-detector/blob/master/isCanvasBlocked.js + +// +export function isCanvasBlocked() { + // create a 1px image data + let blocked = false; + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + // some blockers just return an undefined ctx. So let's check that first. + if (ctx) { + const imageData = ctx.createImageData(1, 1); + const originalImageData = imageData.data; + + // set pixels to RGB 128 + originalImageData[0] = 128; + originalImageData[1] = 128; + originalImageData[2] = 128; + originalImageData[3] = 255; + + // set this to canvas + ctx.putImageData(imageData, 1, 1); + + try { + // now get the data back from canvas. + const checkData = ctx.getImageData(1, 1, 1, 1).data; + + // If this is firefox, and privacy.resistFingerprinting is enabled, + // OR a browser extension blocking the canvas, + // This will return RGB all white (255,255,255) instead of the (128,128,128) we put. + + // so let's check the R and G to see if they're 255 or 128 (matching what we've initially set) + if ( + originalImageData[0] !== checkData[0] && + originalImageData[1] !== checkData[1] + ) { + blocked = true; + } + } catch (error) { + // some extensions will return getImageData null. this is to account for that. + blocked = true; + } + } else { + blocked = true; + } + return blocked; +}