feat: On Android, you can now share text & files from other apps directly into Standard Notes (#2352)
This commit is contained in:
4
packages/mobile/src/CustomAndroidWebView.tsx
Normal file
4
packages/mobile/src/CustomAndroidWebView.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
import { requireNativeComponent } from 'react-native'
|
||||
import { NativeWebViewAndroid } from 'react-native-webview/lib/WebViewTypes'
|
||||
|
||||
export default requireNativeComponent('CustomWebView') as NativeWebViewAndroid
|
||||
@@ -2,6 +2,7 @@ import SNReactNative from '@standardnotes/react-native-utils'
|
||||
import {
|
||||
AppleIAPProductId,
|
||||
AppleIAPReceipt,
|
||||
ApplicationEvent,
|
||||
ApplicationIdentifier,
|
||||
DatabaseKeysLoadChunkResponse,
|
||||
DatabaseLoadOptions,
|
||||
@@ -57,11 +58,13 @@ export enum MobileDeviceEvent {
|
||||
}
|
||||
|
||||
type MobileDeviceEventHandler = (event: MobileDeviceEvent) => void
|
||||
type ApplicationEventHandler = (event: ApplicationEvent) => void
|
||||
|
||||
export class MobileDevice implements MobileDeviceInterface {
|
||||
environment: Environment.Mobile = Environment.Mobile
|
||||
platform: SNPlatform.Ios | SNPlatform.Android = Platform.OS === 'ios' ? SNPlatform.Ios : SNPlatform.Android
|
||||
private eventObservers: MobileDeviceEventHandler[] = []
|
||||
private applicationEventObservers: ApplicationEventHandler[] = []
|
||||
private mobileDeviceEventObservers: MobileDeviceEventHandler[] = []
|
||||
public isDarkMode = false
|
||||
public statusBarBgColor: string | undefined
|
||||
private componentUrls: Map<UuidString, string> = new Map()
|
||||
@@ -346,13 +349,23 @@ export class MobileDevice implements MobileDeviceInterface {
|
||||
}
|
||||
|
||||
performSoftReset() {
|
||||
this.notifyEvent(MobileDeviceEvent.RequestsWebViewReload)
|
||||
this.notifyMobileDeviceEvent(MobileDeviceEvent.RequestsWebViewReload)
|
||||
}
|
||||
|
||||
addMobileWebEventReceiver(handler: MobileDeviceEventHandler): () => void {
|
||||
this.eventObservers.push(handler)
|
||||
addMobileDeviceEventReceiver(handler: MobileDeviceEventHandler): () => void {
|
||||
this.mobileDeviceEventObservers.push(handler)
|
||||
|
||||
const thislessObservers = this.eventObservers
|
||||
const thislessObservers = this.mobileDeviceEventObservers
|
||||
|
||||
return () => {
|
||||
removeFromArray(thislessObservers, handler)
|
||||
}
|
||||
}
|
||||
|
||||
addApplicationEventReceiver(handler: ApplicationEventHandler): () => void {
|
||||
this.applicationEventObservers.push(handler)
|
||||
|
||||
const thislessObservers = this.applicationEventObservers
|
||||
|
||||
return () => {
|
||||
removeFromArray(thislessObservers, handler)
|
||||
@@ -373,8 +386,14 @@ export class MobileDevice implements MobileDeviceInterface {
|
||||
StatusBar.setBarStyle(this.isDarkMode ? 'light-content' : 'dark-content', animated)
|
||||
}
|
||||
|
||||
private notifyEvent(event: MobileDeviceEvent): void {
|
||||
for (const handler of this.eventObservers) {
|
||||
private notifyMobileDeviceEvent(event: MobileDeviceEvent): void {
|
||||
for (const handler of this.mobileDeviceEventObservers) {
|
||||
handler(event)
|
||||
}
|
||||
}
|
||||
|
||||
notifyApplicationEvent(event: ApplicationEvent): void {
|
||||
for (const handler of this.applicationEventObservers) {
|
||||
handler(event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { ReactNativeToWebEvent } from '@standardnotes/snjs'
|
||||
import { ApplicationEvent, ReactNativeToWebEvent } from '@standardnotes/snjs'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Button, Keyboard, Platform, requireNativeComponent, Text, View } from 'react-native'
|
||||
import { Button, Keyboard, Platform, Text, View } from 'react-native'
|
||||
import VersionInfo from 'react-native-version-info'
|
||||
import { WebView, WebViewMessageEvent } from 'react-native-webview'
|
||||
import { NativeWebViewAndroid, OnShouldStartLoadWithRequest } from 'react-native-webview/lib/WebViewTypes'
|
||||
import { OnShouldStartLoadWithRequest } from 'react-native-webview/lib/WebViewTypes'
|
||||
import { AndroidBackHandlerService } from './AndroidBackHandlerService'
|
||||
import { AppStateObserverService } from './AppStateObserverService'
|
||||
import { ColorSchemeObserverService } from './ColorSchemeObserverService'
|
||||
import CustomAndroidWebView from './CustomAndroidWebView'
|
||||
import { MobileDevice, MobileDeviceEvent } from './Lib/MobileDevice'
|
||||
import { IsDev } from './Lib/Utils'
|
||||
|
||||
const CustomWebView: NativeWebViewAndroid = requireNativeComponent('CustomWebView')
|
||||
import { ReceivedSharedItemsHandler } from './ReceivedSharedItemsHandler'
|
||||
|
||||
const LoggingEnabled = IsDev
|
||||
|
||||
@@ -110,7 +110,7 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
|
||||
}, [webViewRef, stateService, device, androidBackHandlerService, colorSchemeService])
|
||||
|
||||
useEffect(() => {
|
||||
const observer = device.addMobileWebEventReceiver((event) => {
|
||||
const observer = device.addMobileDeviceEventReceiver((event) => {
|
||||
if (event === MobileDeviceEvent.RequestsWebViewReload) {
|
||||
destroyAndReload()
|
||||
}
|
||||
@@ -283,6 +283,21 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
|
||||
const requireInlineMediaPlaybackForMomentsFeature = true
|
||||
const requireMediaUserInteractionForMomentsFeature = false
|
||||
|
||||
const receivedSharedItemsHandler = useRef(new ReceivedSharedItemsHandler(webViewRef))
|
||||
useEffect(() => {
|
||||
const receivedSharedItemsHandlerInstance = receivedSharedItemsHandler.current
|
||||
return () => {
|
||||
receivedSharedItemsHandlerInstance.deinit()
|
||||
}
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
return device.addApplicationEventReceiver((event) => {
|
||||
if (event === ApplicationEvent.Launched) {
|
||||
receivedSharedItemsHandler.current.setIsApplicationLaunched(true)
|
||||
}
|
||||
})
|
||||
}, [device])
|
||||
|
||||
if (showAndroidWebviewUpdatePrompt) {
|
||||
return (
|
||||
<View
|
||||
@@ -357,7 +372,7 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
|
||||
overScrollMode="never"
|
||||
nativeConfig={Platform.select({
|
||||
android: {
|
||||
component: CustomWebView,
|
||||
component: CustomAndroidWebView,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
132
packages/mobile/src/ReceivedSharedItemsHandler.ts
Normal file
132
packages/mobile/src/ReceivedSharedItemsHandler.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { ReactNativeToWebEvent } from '@standardnotes/snjs'
|
||||
import { RefObject } from 'react'
|
||||
import { AppState, NativeEventSubscription, NativeModules } from 'react-native'
|
||||
import { readFile } from 'react-native-fs'
|
||||
import WebView from 'react-native-webview'
|
||||
const { ReceiveSharingIntent } = NativeModules
|
||||
|
||||
type ReceivedItem = {
|
||||
contentUri?: string | null
|
||||
fileName?: string | null
|
||||
mimeType?: string | null
|
||||
extension?: string | null
|
||||
text?: string | null
|
||||
weblink?: string | null
|
||||
subject?: string | null
|
||||
}
|
||||
|
||||
type ReceivedFile = ReceivedItem & {
|
||||
contentUri: string
|
||||
mimeType: string
|
||||
}
|
||||
|
||||
type ReceivedWeblink = ReceivedItem & {
|
||||
weblink: string
|
||||
}
|
||||
|
||||
type ReceivedText = ReceivedItem & {
|
||||
text: string
|
||||
}
|
||||
|
||||
const isReceivedFile = (item: ReceivedItem): item is ReceivedFile => {
|
||||
return !!item.contentUri && !!item.mimeType
|
||||
}
|
||||
|
||||
const isReceivedWeblink = (item: ReceivedItem): item is ReceivedWeblink => {
|
||||
return !!item.weblink
|
||||
}
|
||||
|
||||
const isReceivedText = (item: ReceivedItem): item is ReceivedText => {
|
||||
return !!item.text
|
||||
}
|
||||
|
||||
export class ReceivedSharedItemsHandler {
|
||||
private appStateEventSub: NativeEventSubscription | null = null
|
||||
private receivedItemsQueue: ReceivedItem[] = []
|
||||
private isApplicationLaunched = false
|
||||
|
||||
constructor(private webViewRef: RefObject<WebView>) {
|
||||
this.registerNativeEventSub()
|
||||
}
|
||||
|
||||
setIsApplicationLaunched = (isApplicationLaunched: boolean) => {
|
||||
this.isApplicationLaunched = isApplicationLaunched
|
||||
|
||||
if (isApplicationLaunched) {
|
||||
this.handleItemsQueue().catch(console.error)
|
||||
}
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.receivedItemsQueue = []
|
||||
this.appStateEventSub?.remove()
|
||||
}
|
||||
|
||||
private registerNativeEventSub = () => {
|
||||
this.appStateEventSub = AppState.addEventListener('change', (state) => {
|
||||
if (state === 'active') {
|
||||
ReceiveSharingIntent.getFileNames()
|
||||
.then(async (filesObject: Record<string, ReceivedItem>) => {
|
||||
const items = Object.values(filesObject)
|
||||
this.receivedItemsQueue.push(...items)
|
||||
|
||||
if (this.isApplicationLaunched) {
|
||||
this.handleItemsQueue().catch(console.error)
|
||||
}
|
||||
})
|
||||
.then(() => ReceiveSharingIntent.clearFileNames())
|
||||
.catch(console.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleItemsQueue = async () => {
|
||||
if (!this.receivedItemsQueue.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const item = this.receivedItemsQueue.shift()
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isReceivedFile(item)) {
|
||||
const data = await readFile(item.contentUri, 'base64')
|
||||
const file = {
|
||||
name: item.fileName || item.contentUri,
|
||||
data,
|
||||
mimeType: item.mimeType,
|
||||
}
|
||||
this.webViewRef.current?.postMessage(
|
||||
JSON.stringify({
|
||||
reactNativeEvent: ReactNativeToWebEvent.ReceivedFile,
|
||||
messageType: 'event',
|
||||
messageData: file,
|
||||
}),
|
||||
)
|
||||
} else if (isReceivedWeblink(item)) {
|
||||
this.webViewRef.current?.postMessage(
|
||||
JSON.stringify({
|
||||
reactNativeEvent: ReactNativeToWebEvent.ReceivedText,
|
||||
messageType: 'event',
|
||||
messageData: {
|
||||
title: item.subject || item.weblink,
|
||||
text: item.weblink,
|
||||
},
|
||||
}),
|
||||
)
|
||||
} else if (isReceivedText(item)) {
|
||||
this.webViewRef.current?.postMessage(
|
||||
JSON.stringify({
|
||||
reactNativeEvent: ReactNativeToWebEvent.ReceivedText,
|
||||
messageType: 'event',
|
||||
messageData: {
|
||||
text: item.text,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
this.handleItemsQueue().catch(console.error)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user