fix: export/sharing notes on mobile webview (#1644)

This commit is contained in:
Aman Harwara
2022-09-27 12:24:44 +05:30
committed by GitHub
parent c99693876f
commit 6d5ebdeaa1
18 changed files with 298 additions and 43 deletions

View File

@@ -69,7 +69,7 @@
"react-native-screens": "3.13.1", "react-native-screens": "3.13.1",
"react-native-search-bar": "standardnotes/react-native-search-bar#7d2139daf9b7663b570403f21f520deceba9bb09", "react-native-search-bar": "standardnotes/react-native-search-bar#7d2139daf9b7663b570403f21f520deceba9bb09",
"react-native-search-box": "standardnotes/react-native-search-box#8c46369912cba78dca718588aca9c16926953ff7", "react-native-search-box": "standardnotes/react-native-search-box#8c46369912cba78dca718588aca9c16926953ff7",
"react-native-share": "^7.3.7", "react-native-share": "^7.9.0",
"react-native-sodium-jsi": "1.2.0", "react-native-sodium-jsi": "1.2.0",
"react-native-static-server": "standardnotes/react-native-static-server#28ef0175dbee3db9aadfab57498497067556a836", "react-native-static-server": "standardnotes/react-native-static-server#28ef0175dbee3db9aadfab57498497067556a836",
"react-native-store-review": "^0.2.0", "react-native-store-review": "^0.2.0",

View File

@@ -121,11 +121,7 @@ export const AppStackComponent = (props: ModalStackNavigationProp<'AppStack'>) =
[application], [application],
) )
if (!application) { const shouldOpenWebApp = application?.getValue(AlwaysOpenWebAppOnLaunchKey, StorageValueModes.Nonwrapped) as boolean
return null
}
const shouldOpenWebApp = application.getValue(AlwaysOpenWebAppOnLaunchKey, StorageValueModes.Nonwrapped) as boolean
if (IsMobileWeb || shouldOpenWebApp) { if (IsMobileWeb || shouldOpenWebApp) {
return ( return (

View File

@@ -7,16 +7,27 @@ import {
LegacyRawKeychainValue, LegacyRawKeychainValue,
MobileDeviceInterface, MobileDeviceInterface,
NamespacedRootKeyInKeychain, NamespacedRootKeyInKeychain,
Platform as SNPlatform,
RawKeychainValue, RawKeychainValue,
removeFromArray, removeFromArray,
TransferPayload, TransferPayload,
} from '@standardnotes/snjs' } from '@standardnotes/snjs'
import { Alert, Linking, Platform, StatusBar } from 'react-native' import { Alert, Linking, PermissionsAndroid, Platform, StatusBar } from 'react-native'
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 {
CachesDirectoryPath,
DocumentDirectoryPath,
DownloadDirectoryPath,
exists,
unlink,
writeFile,
} from 'react-native-fs'
import { hide, show } from 'react-native-privacy-snapshot' import { hide, show } from 'react-native-privacy-snapshot'
import Share from 'react-native-share'
import { AppStateObserverService } from './../AppStateObserverService' import { AppStateObserverService } from './../AppStateObserverService'
import Keychain from './Keychain' import Keychain from './Keychain'
import { SNReactNativeCrypto } from './ReactNativeCrypto'
import { IsMobileWeb } from './Utils' import { IsMobileWeb } from './Utils'
export type BiometricsType = 'Fingerprint' | 'Face ID' | 'Biometrics' | 'Touch ID' export type BiometricsType = 'Fingerprint' | 'Face ID' | 'Biometrics' | 'Touch ID'
@@ -64,10 +75,14 @@ const showLoadFailForItemIds = (failedItemIds: string[]) => {
export class MobileDevice implements MobileDeviceInterface { export class MobileDevice implements MobileDeviceInterface {
environment: Environment.Mobile = Environment.Mobile environment: Environment.Mobile = Environment.Mobile
platform: SNPlatform.Ios | SNPlatform.Android = Platform.OS === 'ios' ? SNPlatform.Ios : SNPlatform.Android
private eventObservers: MobileDeviceEventHandler[] = [] private eventObservers: MobileDeviceEventHandler[] = []
public isDarkMode = false public isDarkMode = false
private crypto: SNReactNativeCrypto
constructor(private stateObserverService?: AppStateObserverService) {} constructor(private stateObserverService?: AppStateObserverService) {
this.crypto = new SNReactNativeCrypto()
}
deinit() { deinit() {
this.stateObserverService?.deinit() this.stateObserverService?.deinit()
@@ -455,4 +470,76 @@ export class MobileDevice implements MobileDeviceInterface {
isDeviceDestroyed() { isDeviceDestroyed() {
return false return false
} }
async deleteFileAtPathIfExists(path: string) {
if (await exists(path)) {
await unlink(path)
}
}
async shareBase64AsFile(base64: string, filename: string) {
let downloadedTempFilePath: string | undefined
try {
downloadedTempFilePath = await this.downloadBase64AsFile(base64, filename, true)
if (!downloadedTempFilePath) {
return
}
await Share.open({
url: `file://${downloadedTempFilePath}`,
failOnCancel: false,
})
} catch (error) {
this.consoleLog(`${error}`)
} finally {
if (downloadedTempFilePath) {
void this.deleteFileAtPathIfExists(downloadedTempFilePath)
}
}
}
getFileDestinationPath(filename: string, saveInTempLocation: boolean): string {
let directory = DocumentDirectoryPath
if (Platform.OS === 'android') {
directory = saveInTempLocation ? CachesDirectoryPath : DownloadDirectoryPath
}
return `${directory}/${filename}`
}
async hasStoragePermissionOnAndroid(): Promise<boolean> {
if (Platform.OS !== 'android') {
return true
}
const grantedStatus = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE)
if (grantedStatus === PermissionsAndroid.RESULTS.GRANTED) {
return true
}
Alert.alert(
'Storage permissions are required in order to download files. Please accept the permissions prompt and try again.',
)
return false
}
async downloadBase64AsFile(
base64: string,
filename: string,
saveInTempLocation = false,
): Promise<string | undefined> {
const isGrantedStoragePermissionOnAndroid = await this.hasStoragePermissionOnAndroid()
if (!isGrantedStoragePermissionOnAndroid) {
return
}
try {
const path = this.getFileDestinationPath(filename, saveInTempLocation)
void this.deleteFileAtPathIfExists(path)
const decodedContents = this.crypto.base64Decode(base64.replace(/data.*base64,/, ''))
await writeFile(path, decodedContents)
return path
} catch (error) {
this.consoleLog(`${error}`)
}
}
} }

View File

@@ -88,6 +88,7 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
constructor(messageSender) { constructor(messageSender) {
this.appVersion = '${pjson.version} (${VersionInfo.buildVersion})' this.appVersion = '${pjson.version} (${VersionInfo.buildVersion})'
this.environment = 4 this.environment = 4
this.platform = ${device.platform}
this.databases = [] this.databases = []
this.messageSender = messageSender this.messageSender = messageSender
} }

View File

@@ -1,8 +1,9 @@
import { DeviceInterface } from './DeviceInterface' import { DeviceInterface } from './DeviceInterface'
import { Environment, RawKeychainValue } from '@standardnotes/models' import { Environment, Platform, RawKeychainValue } from '@standardnotes/models'
export interface MobileDeviceInterface extends DeviceInterface { export interface MobileDeviceInterface extends DeviceInterface {
environment: Environment.Mobile environment: Environment.Mobile
platform: Platform.Ios | Platform.Android
getRawKeychainValue(): Promise<RawKeychainValue | undefined> getRawKeychainValue(): Promise<RawKeychainValue | undefined>
getDeviceBiometricsAvailability(): Promise<boolean> getDeviceBiometricsAvailability(): Promise<boolean>
@@ -12,4 +13,6 @@ export interface MobileDeviceInterface extends DeviceInterface {
stopHidingMobileInterfaceFromScreenshots(): void stopHidingMobileInterfaceFromScreenshots(): void
consoleLog(...args: any[]): void consoleLog(...args: any[]): void
handleThemeSchemeChange(isDark: boolean): void handleThemeSchemeChange(isDark: boolean): void
shareBase64AsFile(base64: string, filename: string): Promise<void>
downloadBase64AsFile(base64: string, filename: string, saveInTempLocation?: boolean): Promise<string | undefined>
} }

View File

@@ -8,7 +8,7 @@ import {
import { ContentType } from '@standardnotes/common' import { ContentType } from '@standardnotes/common'
import { ApplicationInterface } from '@standardnotes/services' import { ApplicationInterface } from '@standardnotes/services'
function sanitizeFileName(name: string): string { export function sanitizeFileName(name: string): string {
return name.trim().replace(/[.\\/:"?*|<>]/g, '_') return name.trim().replace(/[.\\/:"?*|<>]/g, '_')
} }
@@ -52,13 +52,16 @@ export class ArchiveManager {
}) })
if (encrypted) { if (encrypted) {
this.downloadData(blobData, `Standard Notes Encrypted Backup and Import File - ${this.formattedDate()}.txt`) this.downloadData(
blobData,
`Standard Notes Encrypted Backup and Import File - ${this.formattedDateForExports()}.txt`,
)
} else { } else {
this.downloadZippedDecryptedItems(data).catch(console.error) this.downloadZippedDecryptedItems(data).catch(console.error)
} }
} }
private formattedDate() { formattedDateForExports() {
const string = `${new Date()}` const string = `${new Date()}`
// Match up to the first parenthesis, i.e do not include '(Central Standard Time)' // Match up to the first parenthesis, i.e do not include '(Central Standard Time)'
const matches = string.match(/^(.*?) \(/) const matches = string.match(/^(.*?) \(/)
@@ -108,7 +111,7 @@ export class ArchiveManager {
await nextFile() await nextFile()
} else { } else {
const finalBlob = await zipWriter.close() const finalBlob = await zipWriter.close()
this.downloadData(finalBlob, `Standard Notes Backup - ${this.formattedDate()}.zip`) this.downloadData(finalBlob, `Standard Notes Backup - ${this.formattedDateForExports()}.zip`)
} }
} }
@@ -143,7 +146,7 @@ export class ArchiveManager {
async downloadDataAsZip(data: ZippableData) { async downloadDataAsZip(data: ZippableData) {
const zipFileAsBlob = await this.zipData(data) const zipFileAsBlob = await this.zipData(data)
this.downloadData(zipFileAsBlob, `Standard Notes Export - ${this.formattedDate()}.zip`) this.downloadData(zipFileAsBlob, `Standard Notes Export - ${this.formattedDateForExports()}.zip`)
} }
private hrefForData(data: Blob) { private hrefForData(data: Blob) {

View File

@@ -20,7 +20,7 @@ const createApplication = (
device: WebOrDesktopDevice, device: WebOrDesktopDevice,
webSocketUrl: string, webSocketUrl: string,
) => { ) => {
const platform = getPlatform() const platform = getPlatform(device)
const application = new WebApplication( const application = new WebApplication(
deviceInterface, deviceInterface,

View File

@@ -86,6 +86,7 @@ export const ICONS = {
security: icons.SecurityIcon, security: icons.SecurityIcon,
server: icons.ServerIcon, server: icons.ServerIcon,
settings: icons.SettingsIcon, settings: icons.SettingsIcon,
share: icons.ShareIcon,
signIn: icons.SignInIcon, signIn: icons.SignInIcon,
signOut: icons.SignOutIcon, signOut: icons.SignOutIcon,
spreadsheets: icons.SpreadsheetsIcon, spreadsheets: icons.SpreadsheetsIcon,

View File

@@ -41,7 +41,7 @@ const NotesContextMenu = ({
side="right" side="right"
togglePopover={closeMenu} togglePopover={closeMenu}
> >
<div ref={contextMenuRef}> <div className="select-none" ref={contextMenuRef}>
<NotesOptions <NotesOptions
application={application} application={application}
navigationController={navigationController} navigationController={navigationController}

View File

@@ -2,7 +2,7 @@ import Icon from '@/Components/Icon/Icon'
import Switch from '@/Components/Switch/Switch' import Switch from '@/Components/Switch/Switch'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { useState, useEffect, useMemo, useCallback, FunctionComponent } from 'react' import { useState, useEffect, useMemo, useCallback, FunctionComponent } from 'react'
import { SNApplication, SNComponent, SNNote } from '@standardnotes/snjs' import { Platform, SNApplication, SNComponent, SNNote } from '@standardnotes/snjs'
import { KeyboardModifier } from '@standardnotes/ui-services' import { KeyboardModifier } from '@standardnotes/ui-services'
import ChangeEditorOption from './ChangeEditorOption' import ChangeEditorOption from './ChangeEditorOption'
import { BYTES_IN_ONE_MEGABYTE } from '@/Constants/Constants' import { BYTES_IN_ONE_MEGABYTE } from '@/Constants/Constants'
@@ -15,6 +15,9 @@ import HorizontalSeparator from '../Shared/HorizontalSeparator'
import { formatDateForContextMenu } from '@/Utils/DateUtils' import { formatDateForContextMenu } from '@/Utils/DateUtils'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider' import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata' import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
import { getNoteBlob, getNoteFileName } from '@/Utils/NoteExportUtils'
import { shareSelectedItems } from '@/NativeMobileWeb/ShareSelectedItems'
import { downloadSelectedItemsOnAndroid } from '@/NativeMobileWeb/DownloadSelectedItemsOnAndroid'
type DeletePermanentlyButtonProps = { type DeletePermanentlyButtonProps = {
onClick: () => void onClick: () => void
@@ -95,7 +98,7 @@ const NoteAttributes: FunctionComponent<{
const format = editor?.package_info?.file_type || 'txt' const format = editor?.package_info?.file_type || 'txt'
return ( return (
<div className="px-3 py-1.5 text-xs font-medium text-neutral"> <div className="select-text px-3 py-1.5 text-xs font-medium text-neutral">
{typeof words === 'number' && (format === 'txt' || format === 'md') ? ( {typeof words === 'number' && (format === 'txt' || format === 'md') ? (
<> <>
<div className="mb-1"> <div className="mb-1">
@@ -220,18 +223,11 @@ const NotesOptions = ({
} }
}, [application]) }, [application])
const getNoteFileName = useCallback(
(note: SNNote): string => {
const editor = application.componentManager.editorForNote(note)
const format = editor?.package_info?.file_type || 'txt'
return `${note.title}.${format}`
},
[application.componentManager],
)
const downloadSelectedItems = useCallback(async () => { const downloadSelectedItems = useCallback(async () => {
if (notes.length === 1) { if (notes.length === 1) {
application.getArchiveService().downloadData(new Blob([notes[0].text]), getNoteFileName(notes[0])) const note = notes[0]
const blob = getNoteBlob(application, note)
application.getArchiveService().downloadData(blob, getNoteFileName(application, note))
return return
} }
@@ -243,8 +239,8 @@ const NotesOptions = ({
await application.getArchiveService().downloadDataAsZip( await application.getArchiveService().downloadDataAsZip(
notes.map((note) => { notes.map((note) => {
return { return {
name: getNoteFileName(note), name: getNoteFileName(application, note),
content: new Blob([note.text]), content: getNoteBlob(application, note),
} }
}), }),
) )
@@ -254,7 +250,7 @@ const NotesOptions = ({
message: `Exported ${notes.length} notes`, message: `Exported ${notes.length} notes`,
}) })
} }
}, [application, getNoteFileName, notes]) }, [application, notes])
const closeMenuAndToggleNotesList = useCallback(() => { const closeMenuAndToggleNotesList = useCallback(() => {
toggleAppPane(AppPaneId.Items) toggleAppPane(AppPaneId.Items)
@@ -358,11 +354,22 @@ const NotesOptions = ({
)} )}
<button <button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={downloadSelectedItems} onClick={() => {
application.isNativeMobileWeb() ? shareSelectedItems(application, notes) : downloadSelectedItems()
}}
> >
<Icon type="download" className={iconClass} /> <Icon type={application.platform === Platform.Android ? 'share' : 'download'} className={iconClass} />
Export {application.platform === Platform.Android ? 'Share' : 'Export'}
</button> </button>
{application.platform === Platform.Android && (
<button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => downloadSelectedItemsOnAndroid(application, notes)}
>
<Icon type="download" className={iconClass} />
Export
</button>
)}
<button <button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={duplicateSelectedItems} onClick={duplicateSelectedItems}

View File

@@ -48,7 +48,7 @@ const NotesOptionsPanel = ({
> >
<Icon type="more" /> <Icon type="more" />
</button> </button>
<Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isOpen} className="py-2"> <Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isOpen} className="select-none py-2">
<NotesOptions <NotesOptions
application={application} application={application}
navigationController={navigationController} navigationController={navigationController}

View File

@@ -121,7 +121,7 @@ export const StringUtils = {
if (!application.hasAccount()) { if (!application.hasAccount()) {
return null return null
} }
const platform = getPlatform() const platform = getPlatform(application.deviceInterface)
const keychainName = const keychainName =
platform === Platform.WindowsDesktop platform === Platform.WindowsDesktop
? 'credential manager' ? 'credential manager'

View File

@@ -0,0 +1,67 @@
import { WebApplication } from '@/Application/Application'
import { getBase64FromBlob } from '@/Utils'
import { getNoteBlob, getNoteFileName } from '@/Utils/NoteExportUtils'
import { parseFileName } from '@standardnotes/filepicker'
import { Platform, SNNote } from '@standardnotes/snjs'
import { addToast, dismissToast, ToastType } from '@standardnotes/toast'
import { sanitizeFileName } from '@standardnotes/ui-services'
export const downloadSelectedItemsOnAndroid = async (application: WebApplication, notes: SNNote[]) => {
if (!application.isNativeMobileWeb() || application.platform !== Platform.Android) {
throw new Error('Function being used on non-android platform')
}
if (notes.length === 1) {
const note = notes[0]
const blob = getNoteBlob(application, note)
const base64 = await getBase64FromBlob(blob)
const { name, ext } = parseFileName(getNoteFileName(application, note))
const filename = `${sanitizeFileName(name)}.${ext}`
const loadingToastId = addToast({
type: ToastType.Loading,
message: `Exporting ${filename}..`,
})
const downloaded = await application.mobileDevice.downloadBase64AsFile(base64, filename)
if (downloaded) {
dismissToast(loadingToastId)
addToast({
type: ToastType.Success,
message: `Exported ${filename}`,
})
} else {
addToast({
type: ToastType.Error,
message: `Could not export ${filename}`,
})
}
return
}
if (notes.length > 1) {
const zippedDataBlob = await application.getArchiveService().zipData(
notes.map((note) => {
return {
name: getNoteFileName(application, note),
content: getNoteBlob(application, note),
}
}),
)
const zippedDataAsBase64 = await getBase64FromBlob(zippedDataBlob)
const filename = `Standard Notes Export - ${application.getArchiveService().formattedDateForExports()}.zip`
const loadingToastId = addToast({
type: ToastType.Loading,
message: `Exporting ${filename}..`,
})
const downloaded = await application.mobileDevice.downloadBase64AsFile(zippedDataAsBase64, filename)
if (downloaded) {
dismissToast(loadingToastId)
addToast({
type: ToastType.Success,
message: `Exported ${filename}`,
})
} else {
addToast({
type: ToastType.Error,
message: `Could not export ${filename}`,
})
}
}
}

View File

@@ -0,0 +1,36 @@
import { WebApplication } from '@/Application/Application'
import { getBase64FromBlob } from '@/Utils'
import { getNoteBlob, getNoteFileName } from '@/Utils/NoteExportUtils'
import { parseFileName } from '@standardnotes/filepicker'
import { SNNote } from '@standardnotes/snjs'
import { sanitizeFileName } from '@standardnotes/ui-services'
export const shareSelectedItems = async (application: WebApplication, notes: SNNote[]) => {
if (!application.isNativeMobileWeb()) {
throw new Error('Share function being used outside mobile webview')
}
if (notes.length === 1) {
const note = notes[0]
const blob = getNoteBlob(application, note)
const base64 = await getBase64FromBlob(blob)
const { name, ext } = parseFileName(getNoteFileName(application, note))
const filename = `${sanitizeFileName(name)}.${ext}`
application.mobileDevice.shareBase64AsFile(base64, filename)
return
}
if (notes.length > 1) {
const zippedDataBlob = await application.getArchiveService().zipData(
notes.map((note) => {
return {
name: getNoteFileName(application, note),
content: getNoteBlob(application, note),
}
}),
)
const zippedDataAsBase64 = await getBase64FromBlob(zippedDataBlob)
application.mobileDevice.shareBase64AsFile(
zippedDataAsBase64,
`Standard Notes Export - ${application.getArchiveService().formattedDateForExports()}.zip`,
)
}
}

View File

@@ -0,0 +1,36 @@
import { WebApplication } from '@/Application/Application'
import { SNNote } from '@standardnotes/snjs'
export const getNoteFormat = (application: WebApplication, note: SNNote) => {
const editor = application.componentManager.editorForNote(note)
const format = editor?.package_info?.file_type || 'txt'
return format
}
export const getNoteFileName = (application: WebApplication, note: SNNote): string => {
const format = getNoteFormat(application, note)
return `${note.title}.${format}`
}
export const getNoteBlob = (application: WebApplication, note: SNNote) => {
const format = getNoteFormat(application, note)
let type: string
switch (format) {
case 'html':
type = 'text/html'
break
case 'json':
type = 'application/json'
break
case 'md':
type = 'text/markdown'
break
default:
type = 'text/plain'
break
}
const blob = new Blob([note.text], {
type,
})
return blob
}

View File

@@ -1,4 +1,4 @@
import { Platform, platformFromString } from '@standardnotes/snjs' import { DeviceInterface, MobileDeviceInterface, Platform, platformFromString } from '@standardnotes/snjs'
import { IsDesktopPlatform, IsWebPlatform } from '@/Constants/Version' import { IsDesktopPlatform, IsWebPlatform } from '@/Constants/Version'
import { EMAIL_REGEX } from '../Constants/Constants' import { EMAIL_REGEX } from '../Constants/Constants'
import { MediaQueryBreakpoints } from '@/Hooks/useMediaQuery' import { MediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
@@ -31,7 +31,11 @@ export function getPlatformString() {
} }
} }
export function getPlatform(): Platform { export function getPlatform(device: DeviceInterface | MobileDeviceInterface): Platform {
if ('platform' in device) {
return device.platform
}
return platformFromString(getPlatformString()) return platformFromString(getPlatformString())
} }
@@ -204,3 +208,17 @@ export const disableIosTextFieldZoom = () => {
} }
export const isMobileScreen = () => !window.matchMedia(MediaQueryBreakpoints.md).matches export const isMobileScreen = () => !window.matchMedia(MediaQueryBreakpoints.md).matches
export const getBase64FromBlob = (blob: Blob) => {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader()
reader.onloadend = () => {
if (reader.result) {
resolve(reader.result.toString())
} else {
reject()
}
}
reader.readAsDataURL(blob)
})
}

View File

@@ -7195,7 +7195,7 @@ __metadata:
react-native-screens: 3.13.1 react-native-screens: 3.13.1
react-native-search-bar: "standardnotes/react-native-search-bar#7d2139daf9b7663b570403f21f520deceba9bb09" react-native-search-bar: "standardnotes/react-native-search-bar#7d2139daf9b7663b570403f21f520deceba9bb09"
react-native-search-box: "standardnotes/react-native-search-box#8c46369912cba78dca718588aca9c16926953ff7" react-native-search-box: "standardnotes/react-native-search-box#8c46369912cba78dca718588aca9c16926953ff7"
react-native-share: ^7.3.7 react-native-share: ^7.9.0
react-native-sodium-jsi: 1.2.0 react-native-sodium-jsi: 1.2.0
react-native-static-server: "standardnotes/react-native-static-server#28ef0175dbee3db9aadfab57498497067556a836" react-native-static-server: "standardnotes/react-native-static-server#28ef0175dbee3db9aadfab57498497067556a836"
react-native-store-review: ^0.2.0 react-native-store-review: ^0.2.0
@@ -32971,10 +32971,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-native-share@npm:^7.3.7": "react-native-share@npm:^7.9.0":
version: 7.6.4 version: 7.9.0
resolution: "react-native-share@npm:7.6.4" resolution: "react-native-share@npm:7.9.0"
checksum: 22764354738587d9166653b09e014ddecf54562696b7f92353980b12be0f9adef58c5d9f927bd05a5e19f50956e6f14fbe2b7a3105675e5982823512945be315 checksum: 8eb2f5b4be8df11224f8f00b2761c2c3b4231590d945f57e8ed91899a455d7b5083bb62aa8a017ed8c15fbdfb95e85f9ee4060ac00c01897ef5c351e756c46af
languageName: node languageName: node
linkType: hard linkType: hard