feat: use native preview for pdf in mobile webview (#1728)
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
|||||||
TransferPayload,
|
TransferPayload,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { Alert, Linking, PermissionsAndroid, Platform, StatusBar } from 'react-native'
|
import { Alert, Linking, PermissionsAndroid, Platform, StatusBar } from 'react-native'
|
||||||
|
import FileViewer from 'react-native-file-viewer'
|
||||||
import FingerprintScanner from 'react-native-fingerprint-scanner'
|
import FingerprintScanner from 'react-native-fingerprint-scanner'
|
||||||
import FlagSecure from 'react-native-flag-secure-android'
|
import FlagSecure from 'react-native-flag-secure-android'
|
||||||
import {
|
import {
|
||||||
@@ -541,7 +542,7 @@ export class MobileDevice implements MobileDeviceInterface {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const path = this.getFileDestinationPath(filename, saveInTempLocation)
|
const path = this.getFileDestinationPath(filename, saveInTempLocation)
|
||||||
void this.deleteFileAtPathIfExists(path)
|
await this.deleteFileAtPathIfExists(path)
|
||||||
await writeFile(path, base64.replace(/data.*base64,/, ''), 'base64')
|
await writeFile(path, base64.replace(/data.*base64,/, ''), 'base64')
|
||||||
return path
|
return path
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -549,6 +550,28 @@ export class MobileDevice implements MobileDeviceInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async previewFile(base64: string, filename: string): Promise<boolean> {
|
||||||
|
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() {
|
confirmAndExit() {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
'Close app',
|
'Close app',
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export interface WebApplicationInterface extends ApplicationInterface {
|
|||||||
handleMobileLosingFocusEvent(): Promise<void>
|
handleMobileLosingFocusEvent(): Promise<void>
|
||||||
handleMobileResumingFromBackgroundEvent(): Promise<void>
|
handleMobileResumingFromBackgroundEvent(): Promise<void>
|
||||||
isNativeMobileWeb(): boolean
|
isNativeMobileWeb(): boolean
|
||||||
get mobileDevice(): MobileDeviceInterface
|
mobileDevice(): MobileDeviceInterface
|
||||||
handleAndroidBackButtonPressed(): void
|
handleAndroidBackButtonPressed(): void
|
||||||
addAndroidBackHandlerEventListener(listener: () => boolean): (() => void) | undefined
|
addAndroidBackHandlerEventListener(listener: () => boolean): (() => void) | undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,5 +15,6 @@ export interface MobileDeviceInterface extends DeviceInterface {
|
|||||||
handleThemeSchemeChange(isDark: boolean, bgColor: string): void
|
handleThemeSchemeChange(isDark: boolean, bgColor: string): void
|
||||||
shareBase64AsFile(base64: string, filename: string): Promise<void>
|
shareBase64AsFile(base64: string, filename: string): Promise<void>
|
||||||
downloadBase64AsFile(base64: string, filename: string, saveInTempLocation?: boolean): Promise<string | undefined>
|
downloadBase64AsFile(base64: string, filename: string, saveInTempLocation?: boolean): Promise<string | undefined>
|
||||||
|
previewFile(base64: string, filename: string): Promise<boolean>
|
||||||
confirmAndExit(): void
|
confirmAndExit(): void
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -293,10 +293,9 @@ export class ThemeManager extends AbstractService {
|
|||||||
|
|
||||||
if (this.application.isNativeMobileWeb()) {
|
if (this.application.isNativeMobileWeb()) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.application.mobileDevice.handleThemeSchemeChange(
|
this.application
|
||||||
theme.package_info.isDark ?? false,
|
.mobileDevice()
|
||||||
this.getBackgroundColor(),
|
.handleThemeSchemeChange(theme.package_info.isDark ?? false, this.getBackgroundColor())
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,7 +334,7 @@ export class ThemeManager extends AbstractService {
|
|||||||
removeFromArray(this.activeThemes, uuid)
|
removeFromArray(this.activeThemes, uuid)
|
||||||
|
|
||||||
if (this.activeThemes.length === 0 && this.application.isNativeMobileWeb()) {
|
if (this.activeThemes.length === 0 && this.application.isNativeMobileWeb()) {
|
||||||
this.application.mobileDevice.handleThemeSchemeChange(false, '#ffffff')
|
this.application.mobileDevice().handleThemeSchemeChange(false, '#ffffff')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log = (...args) => {
|
console.log = (...args) => {
|
||||||
this.mobileDevice.consoleLog(...args)
|
this.mobileDevice().consoleLog(...args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
get mobileDevice(): MobileDeviceInterface {
|
mobileDevice(): MobileDeviceInterface {
|
||||||
if (!this.isNativeMobileWeb()) {
|
if (!this.isNativeMobileWeb()) {
|
||||||
throw Error('Attempting to access device as mobile device on non mobile platform')
|
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<void> {
|
async handleMobileLosingFocusEvent(): Promise<void> {
|
||||||
if (this.getMobileScreenshotPrivacyEnabled()) {
|
if (this.getMobileScreenshotPrivacyEnabled()) {
|
||||||
this.mobileDevice.stopHidingMobileInterfaceFromScreenshots()
|
this.mobileDevice().stopHidingMobileInterfaceFromScreenshots()
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.lockApplicationAfterMobileEventIfApplicable()
|
await this.lockApplicationAfterMobileEventIfApplicable()
|
||||||
@@ -246,7 +246,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
|
|
||||||
async handleMobileResumingFromBackgroundEvent(): Promise<void> {
|
async handleMobileResumingFromBackgroundEvent(): Promise<void> {
|
||||||
if (this.getMobileScreenshotPrivacyEnabled()) {
|
if (this.getMobileScreenshotPrivacyEnabled()) {
|
||||||
this.mobileDevice.hideMobileInterfaceFromScreenshots()
|
this.mobileDevice().hideMobileInterfaceFromScreenshots()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const BiometricsPrompt = ({ application, onValueChange, prompt, buttonRef }: Pro
|
|||||||
fullWidth
|
fullWidth
|
||||||
colorStyle={authenticated ? 'success' : 'info'}
|
colorStyle={authenticated ? 'success' : 'info'}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const authenticated = await application.mobileDevice.authenticateWithBiometrics()
|
const authenticated = await application.mobileDevice().authenticateWithBiometrics()
|
||||||
setAuthenticated(authenticated)
|
setAuthenticated(authenticated)
|
||||||
onValueChange(authenticated, prompt)
|
onValueChange(authenticated, prompt)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ const FilePreview = ({ file, application }: Props) => {
|
|||||||
<span className="mt-3">Loading file...</span>
|
<span className="mt-3">Loading file...</span>
|
||||||
</div>
|
</div>
|
||||||
) : downloadedBytes ? (
|
) : downloadedBytes ? (
|
||||||
<PreviewComponent file={file} bytes={downloadedBytes} />
|
<PreviewComponent application={application} file={file} bytes={downloadedBytes} />
|
||||||
) : (
|
) : (
|
||||||
<FilePreviewError
|
<FilePreviewError
|
||||||
file={file}
|
file={file}
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
|
import { WebApplication } from '@/Application/Application'
|
||||||
|
import { getBase64FromBlob } from '@/Utils'
|
||||||
import { FileItem } from '@standardnotes/snjs'
|
import { FileItem } from '@standardnotes/snjs'
|
||||||
import { FunctionComponent, useEffect, useMemo, useRef } from 'react'
|
import { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||||
|
import Button from '../Button/Button'
|
||||||
|
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||||
|
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||||
import { createObjectURLWithRef } from './CreateObjectURLWithRef'
|
import { createObjectURLWithRef } from './CreateObjectURLWithRef'
|
||||||
import ImagePreview from './ImagePreview'
|
import ImagePreview from './ImagePreview'
|
||||||
import { PreviewableTextFileTypes } from './isFilePreviewable'
|
import { PreviewableTextFileTypes, RequiresNativeFilePreview } from './isFilePreviewable'
|
||||||
import TextPreview from './TextPreview'
|
import TextPreview from './TextPreview'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
application: WebApplication
|
||||||
file: FileItem
|
file: FileItem
|
||||||
bytes: Uint8Array
|
bytes: Uint8Array
|
||||||
}
|
}
|
||||||
|
|
||||||
const PreviewComponent: FunctionComponent<Props> = ({ file, bytes }) => {
|
const PreviewComponent: FunctionComponent<Props> = ({ application, file, bytes }) => {
|
||||||
|
const { selectedPane } = useResponsiveAppPane()
|
||||||
|
|
||||||
const objectUrlRef = useRef<string>()
|
const objectUrlRef = useRef<string>()
|
||||||
|
|
||||||
const objectUrl = useMemo(() => {
|
const objectUrl = useMemo(() => {
|
||||||
@@ -28,6 +36,42 @@ const PreviewComponent: FunctionComponent<Props> = ({ 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 (
|
||||||
|
<div className="flex flex-grow flex-col items-center justify-center">
|
||||||
|
<div className="text-base font-bold">Previewing file...</div>
|
||||||
|
<Button className="mt-3" primary onClick={openNativeFilePreview}>
|
||||||
|
Re-open file preview
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (file.mimeType.startsWith('image/')) {
|
if (file.mimeType.startsWith('image/')) {
|
||||||
return <ImagePreview objectUrl={objectUrl} />
|
return <ImagePreview objectUrl={objectUrl} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export const PreviewableTextFileTypes = ['text/plain', 'text/csv', 'application/json']
|
export const PreviewableTextFileTypes = ['text/plain', 'text/csv', 'application/json']
|
||||||
|
|
||||||
|
export const RequiresNativeFilePreview = ['application/pdf']
|
||||||
|
|
||||||
export const isFileTypePreviewable = (fileType: string) => {
|
export const isFileTypePreviewable = (fileType: string) => {
|
||||||
const isImage = fileType.startsWith('image/')
|
const isImage = fileType.startsWith('image/')
|
||||||
const isVideo = fileType.startsWith('video/')
|
const isVideo = fileType.startsWith('video/')
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const UpgradeNow = ({ application, featuresController }: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (application.isNativeMobileWeb()) {
|
if (application.isNativeMobileWeb()) {
|
||||||
application.mobileDevice.openUrl(window.plansUrl)
|
application.mobileDevice().openUrl(window.plansUrl)
|
||||||
} else {
|
} else {
|
||||||
window.location.assign(window.plansUrl)
|
window.location.assign(window.plansUrl)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const CustomNoteTitleFormat = ({ application }: Props) => {
|
|||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
if (application.isNativeMobileWeb()) {
|
if (application.isNativeMobileWeb()) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
application.mobileDevice.openUrl(HelpPageUrl)
|
application.mobileDevice().openUrl(HelpPageUrl)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const loadPurchaseFlowUrl = async (application: WebApplication): Promise<
|
|||||||
const finalUrl = `${url}${period}${plan}`
|
const finalUrl = `${url}${period}${plan}`
|
||||||
|
|
||||||
if (application.isNativeMobileWeb()) {
|
if (application.isNativeMobileWeb()) {
|
||||||
application.mobileDevice.openUrl(finalUrl)
|
application.mobileDevice().openUrl(finalUrl)
|
||||||
} else {
|
} else {
|
||||||
window.location.assign(finalUrl)
|
window.location.assign(finalUrl)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const downloadBlobOnAndroid = async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
const base64 = await getBase64FromBlob(blob)
|
const base64 = await getBase64FromBlob(blob)
|
||||||
const downloaded = await application.mobileDevice.downloadBase64AsFile(base64, filename)
|
const downloaded = await application.mobileDevice().downloadBase64AsFile(base64, filename)
|
||||||
if (loadingToastId) {
|
if (loadingToastId) {
|
||||||
dismissToast(loadingToastId)
|
dismissToast(loadingToastId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ export const shareBlobOnMobile = async (application: WebApplication, blob: Blob,
|
|||||||
throw new Error('Share function being used outside mobile webview')
|
throw new Error('Share function being used outside mobile webview')
|
||||||
}
|
}
|
||||||
const base64 = await getBase64FromBlob(blob)
|
const base64 = await getBase64FromBlob(blob)
|
||||||
application.mobileDevice.shareBase64AsFile(base64, filename)
|
application.mobileDevice().shareBase64AsFile(base64, filename)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const AndroidBackHandlerProvider = ({ application, children }: ProviderProps) =>
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const removeListener = addAndroidBackHandler(() => {
|
const removeListener = addAndroidBackHandler(() => {
|
||||||
application.mobileDevice.confirmAndExit()
|
application.mobileDevice().confirmAndExit()
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user