From a90e4a50e820aa13b6af1f76ee9b2f935d12f211 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Sun, 2 Oct 2022 23:10:44 +0530 Subject: [PATCH] feat: use native preview for pdf in mobile webview (#1728) --- packages/mobile/src/Lib/Interface.ts | 25 +++++++++- .../Application/WebApplicationInterface.ts | 2 +- .../Domain/Device/MobileDeviceInterface.ts | 1 + .../ui-services/src/Theme/ThemeManager.ts | 9 ++-- .../javascripts/Application/Application.ts | 8 +-- .../ChallengeModal/BiometricsPrompt.tsx | 2 +- .../Components/FilePreview/FilePreview.tsx | 2 +- .../FilePreview/PreviewComponent.tsx | 50 +++++++++++++++++-- .../FilePreview/isFilePreviewable.ts | 2 + .../Components/Footer/UpgradeNow.tsx | 2 +- .../Defaults/CustomNoteTitleFormat.tsx | 2 +- .../PurchaseFlow/PurchaseFlowFunctions.ts | 2 +- .../NativeMobileWeb/DownloadBlobOnAndroid.tsx | 2 +- .../NativeMobileWeb/ShareBlobOnMobile.ts | 2 +- .../NativeMobileWeb/useAndroidBackHandler.tsx | 2 +- 15 files changed, 91 insertions(+), 22 deletions(-) diff --git a/packages/mobile/src/Lib/Interface.ts b/packages/mobile/src/Lib/Interface.ts index de6a684f1..f02fea8a6 100644 --- a/packages/mobile/src/Lib/Interface.ts +++ b/packages/mobile/src/Lib/Interface.ts @@ -14,6 +14,7 @@ import { TransferPayload, } from '@standardnotes/snjs' import { Alert, Linking, PermissionsAndroid, Platform, StatusBar } from 'react-native' +import FileViewer from 'react-native-file-viewer' import FingerprintScanner from 'react-native-fingerprint-scanner' import FlagSecure from 'react-native-flag-secure-android' import { @@ -541,7 +542,7 @@ export class MobileDevice implements MobileDeviceInterface { try { const path = this.getFileDestinationPath(filename, saveInTempLocation) - void this.deleteFileAtPathIfExists(path) + await this.deleteFileAtPathIfExists(path) await writeFile(path, base64.replace(/data.*base64,/, ''), 'base64') return path } catch (error) { @@ -549,6 +550,28 @@ export class MobileDevice implements MobileDeviceInterface { } } + async previewFile(base64: string, filename: string): Promise { + const tempLocation = await this.downloadBase64AsFile(base64, filename, true) + + if (!tempLocation) { + this.consoleLog('Error: Could not download file to preview') + return false + } + + try { + await FileViewer.open(tempLocation, { + onDismiss: async () => { + await this.deleteFileAtPathIfExists(tempLocation) + }, + }) + } catch (error) { + this.consoleLog(error) + return false + } + + return true + } + confirmAndExit() { Alert.alert( 'Close app', diff --git a/packages/services/src/Domain/Application/WebApplicationInterface.ts b/packages/services/src/Domain/Application/WebApplicationInterface.ts index 9e80b8a0e..36d3d8eac 100644 --- a/packages/services/src/Domain/Application/WebApplicationInterface.ts +++ b/packages/services/src/Domain/Application/WebApplicationInterface.ts @@ -11,7 +11,7 @@ export interface WebApplicationInterface extends ApplicationInterface { handleMobileLosingFocusEvent(): Promise handleMobileResumingFromBackgroundEvent(): Promise isNativeMobileWeb(): boolean - get mobileDevice(): MobileDeviceInterface + mobileDevice(): MobileDeviceInterface handleAndroidBackButtonPressed(): void addAndroidBackHandlerEventListener(listener: () => boolean): (() => void) | undefined } diff --git a/packages/services/src/Domain/Device/MobileDeviceInterface.ts b/packages/services/src/Domain/Device/MobileDeviceInterface.ts index b4b6d08e6..0a26c8278 100644 --- a/packages/services/src/Domain/Device/MobileDeviceInterface.ts +++ b/packages/services/src/Domain/Device/MobileDeviceInterface.ts @@ -15,5 +15,6 @@ export interface MobileDeviceInterface extends DeviceInterface { handleThemeSchemeChange(isDark: boolean, bgColor: string): void shareBase64AsFile(base64: string, filename: string): Promise downloadBase64AsFile(base64: string, filename: string, saveInTempLocation?: boolean): Promise + previewFile(base64: string, filename: string): Promise confirmAndExit(): void } diff --git a/packages/ui-services/src/Theme/ThemeManager.ts b/packages/ui-services/src/Theme/ThemeManager.ts index 5e39372f8..caccee11e 100644 --- a/packages/ui-services/src/Theme/ThemeManager.ts +++ b/packages/ui-services/src/Theme/ThemeManager.ts @@ -293,10 +293,9 @@ export class ThemeManager extends AbstractService { if (this.application.isNativeMobileWeb()) { setTimeout(() => { - this.application.mobileDevice.handleThemeSchemeChange( - theme.package_info.isDark ?? false, - this.getBackgroundColor(), - ) + this.application + .mobileDevice() + .handleThemeSchemeChange(theme.package_info.isDark ?? false, this.getBackgroundColor()) }) } } @@ -335,7 +334,7 @@ export class ThemeManager extends AbstractService { removeFromArray(this.activeThemes, uuid) if (this.activeThemes.length === 0 && this.application.isNativeMobileWeb()) { - this.application.mobileDevice.handleThemeSchemeChange(false, '#ffffff') + this.application.mobileDevice().handleThemeSchemeChange(false, '#ffffff') } } diff --git a/packages/web/src/javascripts/Application/Application.ts b/packages/web/src/javascripts/Application/Application.ts index 6ddbb68bf..310546234 100644 --- a/packages/web/src/javascripts/Application/Application.ts +++ b/packages/web/src/javascripts/Application/Application.ts @@ -83,7 +83,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter // eslint-disable-next-line no-console console.log = (...args) => { - this.mobileDevice.consoleLog(...args) + this.mobileDevice().consoleLog(...args) } } @@ -179,7 +179,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter return undefined } - get mobileDevice(): MobileDeviceInterface { + mobileDevice(): MobileDeviceInterface { if (!this.isNativeMobileWeb()) { throw Error('Attempting to access device as mobile device on non mobile platform') } @@ -238,7 +238,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter async handleMobileLosingFocusEvent(): Promise { if (this.getMobileScreenshotPrivacyEnabled()) { - this.mobileDevice.stopHidingMobileInterfaceFromScreenshots() + this.mobileDevice().stopHidingMobileInterfaceFromScreenshots() } await this.lockApplicationAfterMobileEventIfApplicable() @@ -246,7 +246,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter async handleMobileResumingFromBackgroundEvent(): Promise { if (this.getMobileScreenshotPrivacyEnabled()) { - this.mobileDevice.hideMobileInterfaceFromScreenshots() + this.mobileDevice().hideMobileInterfaceFromScreenshots() } } diff --git a/packages/web/src/javascripts/Components/ChallengeModal/BiometricsPrompt.tsx b/packages/web/src/javascripts/Components/ChallengeModal/BiometricsPrompt.tsx index b0be14112..5f27ce0f8 100644 --- a/packages/web/src/javascripts/Components/ChallengeModal/BiometricsPrompt.tsx +++ b/packages/web/src/javascripts/Components/ChallengeModal/BiometricsPrompt.tsx @@ -22,7 +22,7 @@ const BiometricsPrompt = ({ application, onValueChange, prompt, buttonRef }: Pro fullWidth colorStyle={authenticated ? 'success' : 'info'} onClick={async () => { - const authenticated = await application.mobileDevice.authenticateWithBiometrics() + const authenticated = await application.mobileDevice().authenticateWithBiometrics() setAuthenticated(authenticated) onValueChange(authenticated, prompt) }} diff --git a/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx b/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx index 4de7cc38a..aa771ec21 100644 --- a/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx +++ b/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx @@ -66,7 +66,7 @@ const FilePreview = ({ file, application }: Props) => { Loading file... ) : downloadedBytes ? ( - + ) : ( = ({ file, bytes }) => { +const PreviewComponent: FunctionComponent = ({ application, file, bytes }) => { + const { selectedPane } = useResponsiveAppPane() + const objectUrlRef = useRef() const objectUrl = useMemo(() => { @@ -28,6 +36,42 @@ const PreviewComponent: FunctionComponent = ({ file, bytes }) => { } }, []) + const isNativeMobileWeb = application.isNativeMobileWeb() + const requiresNativePreview = RequiresNativeFilePreview.includes(file.mimeType) + + const openNativeFilePreview = useCallback(async () => { + if (!isNativeMobileWeb) { + throw new Error('Native file preview cannot be used on non-native platform') + } + + const fileBase64 = await getBase64FromBlob( + new Blob([bytes], { + type: file.mimeType, + }), + ) + + application.mobileDevice().previewFile(fileBase64, file.name) + }, [application, bytes, file.mimeType, file.name, isNativeMobileWeb]) + + useEffect(() => { + const shouldOpenNativePreviewOnLoad = + isNativeMobileWeb && selectedPane === AppPaneId.Editor && requiresNativePreview + if (shouldOpenNativePreviewOnLoad) { + void openNativeFilePreview() + } + }, [isNativeMobileWeb, openNativeFilePreview, requiresNativePreview, selectedPane]) + + if (isNativeMobileWeb && requiresNativePreview) { + return ( +
+
Previewing file...
+ +
+ ) + } + if (file.mimeType.startsWith('image/')) { return } diff --git a/packages/web/src/javascripts/Components/FilePreview/isFilePreviewable.ts b/packages/web/src/javascripts/Components/FilePreview/isFilePreviewable.ts index f1cd74bae..8a4987a17 100644 --- a/packages/web/src/javascripts/Components/FilePreview/isFilePreviewable.ts +++ b/packages/web/src/javascripts/Components/FilePreview/isFilePreviewable.ts @@ -1,5 +1,7 @@ export const PreviewableTextFileTypes = ['text/plain', 'text/csv', 'application/json'] +export const RequiresNativeFilePreview = ['application/pdf'] + export const isFileTypePreviewable = (fileType: string) => { const isImage = fileType.startsWith('image/') const isVideo = fileType.startsWith('video/') diff --git a/packages/web/src/javascripts/Components/Footer/UpgradeNow.tsx b/packages/web/src/javascripts/Components/Footer/UpgradeNow.tsx index 12fde3e9b..5218a412c 100644 --- a/packages/web/src/javascripts/Components/Footer/UpgradeNow.tsx +++ b/packages/web/src/javascripts/Components/Footer/UpgradeNow.tsx @@ -18,7 +18,7 @@ const UpgradeNow = ({ application, featuresController }: Props) => { } if (application.isNativeMobileWeb()) { - application.mobileDevice.openUrl(window.plansUrl) + application.mobileDevice().openUrl(window.plansUrl) } else { window.location.assign(window.plansUrl) } diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/General/Defaults/CustomNoteTitleFormat.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/General/Defaults/CustomNoteTitleFormat.tsx index 995206c80..3498f560c 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/General/Defaults/CustomNoteTitleFormat.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/General/Defaults/CustomNoteTitleFormat.tsx @@ -51,7 +51,7 @@ const CustomNoteTitleFormat = ({ application }: Props) => { onClick={(event) => { if (application.isNativeMobileWeb()) { event.preventDefault() - application.mobileDevice.openUrl(HelpPageUrl) + application.mobileDevice().openUrl(HelpPageUrl) } }} > diff --git a/packages/web/src/javascripts/Components/PurchaseFlow/PurchaseFlowFunctions.ts b/packages/web/src/javascripts/Components/PurchaseFlow/PurchaseFlowFunctions.ts index 48b742bc1..20f12f140 100644 --- a/packages/web/src/javascripts/Components/PurchaseFlow/PurchaseFlowFunctions.ts +++ b/packages/web/src/javascripts/Components/PurchaseFlow/PurchaseFlowFunctions.ts @@ -23,7 +23,7 @@ export const loadPurchaseFlowUrl = async (application: WebApplication): Promise< const finalUrl = `${url}${period}${plan}` if (application.isNativeMobileWeb()) { - application.mobileDevice.openUrl(finalUrl) + application.mobileDevice().openUrl(finalUrl) } else { window.location.assign(finalUrl) } diff --git a/packages/web/src/javascripts/NativeMobileWeb/DownloadBlobOnAndroid.tsx b/packages/web/src/javascripts/NativeMobileWeb/DownloadBlobOnAndroid.tsx index eba4caec3..324b51f85 100644 --- a/packages/web/src/javascripts/NativeMobileWeb/DownloadBlobOnAndroid.tsx +++ b/packages/web/src/javascripts/NativeMobileWeb/DownloadBlobOnAndroid.tsx @@ -20,7 +20,7 @@ export const downloadBlobOnAndroid = async ( }) } const base64 = await getBase64FromBlob(blob) - const downloaded = await application.mobileDevice.downloadBase64AsFile(base64, filename) + const downloaded = await application.mobileDevice().downloadBase64AsFile(base64, filename) if (loadingToastId) { dismissToast(loadingToastId) } diff --git a/packages/web/src/javascripts/NativeMobileWeb/ShareBlobOnMobile.ts b/packages/web/src/javascripts/NativeMobileWeb/ShareBlobOnMobile.ts index 904c97b0e..355370fc0 100644 --- a/packages/web/src/javascripts/NativeMobileWeb/ShareBlobOnMobile.ts +++ b/packages/web/src/javascripts/NativeMobileWeb/ShareBlobOnMobile.ts @@ -6,5 +6,5 @@ export const shareBlobOnMobile = async (application: WebApplication, blob: Blob, throw new Error('Share function being used outside mobile webview') } const base64 = await getBase64FromBlob(blob) - application.mobileDevice.shareBase64AsFile(base64, filename) + application.mobileDevice().shareBase64AsFile(base64, filename) } diff --git a/packages/web/src/javascripts/NativeMobileWeb/useAndroidBackHandler.tsx b/packages/web/src/javascripts/NativeMobileWeb/useAndroidBackHandler.tsx index 3e0ccbcab..4f0313042 100644 --- a/packages/web/src/javascripts/NativeMobileWeb/useAndroidBackHandler.tsx +++ b/packages/web/src/javascripts/NativeMobileWeb/useAndroidBackHandler.tsx @@ -34,7 +34,7 @@ const AndroidBackHandlerProvider = ({ application, children }: ProviderProps) => useEffect(() => { const removeListener = addAndroidBackHandler(() => { - application.mobileDevice.confirmAndExit() + application.mobileDevice().confirmAndExit() return true }) return () => {