diff --git a/packages/mobile/android/app/src/main/AndroidManifest.xml b/packages/mobile/android/app/src/main/AndroidManifest.xml
index 40bff1c06..a9ff71660 100644
--- a/packages/mobile/android/app/src/main/AndroidManifest.xml
+++ b/packages/mobile/android/app/src/main/AndroidManifest.xml
@@ -43,6 +43,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/mobile/android/app/src/main/java/com/standardnotes/MainActivity.java b/packages/mobile/android/app/src/main/java/com/standardnotes/MainActivity.java
index 9a4d0dd35..eeeec1ad9 100644
--- a/packages/mobile/android/app/src/main/java/com/standardnotes/MainActivity.java
+++ b/packages/mobile/android/app/src/main/java/com/standardnotes/MainActivity.java
@@ -1,6 +1,7 @@
package com.standardnotes;
import android.content.Context;
+import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.content.res.Configuration;
@@ -54,4 +55,10 @@ public class MainActivity extends ReactActivity {
public void invokeDefaultOnBackPressed() {
moveTaskToBack(true);
}
+
+ @Override
+ public void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ setIntent(intent);
+ }
}
diff --git a/packages/mobile/android/app/src/main/java/com/standardnotes/MainApplication.java b/packages/mobile/android/app/src/main/java/com/standardnotes/MainApplication.java
index 7461358fa..48c9f84a5 100644
--- a/packages/mobile/android/app/src/main/java/com/standardnotes/MainApplication.java
+++ b/packages/mobile/android/app/src/main/java/com/standardnotes/MainApplication.java
@@ -39,6 +39,7 @@ public class MainApplication extends Application implements ReactApplication {
packages.add(new Fido2ApiPackage());
packages.add(new CustomWebViewPackage());
+ packages.add(new ReceiveSharingIntentPackage());
return packages;
}
diff --git a/packages/mobile/android/app/src/main/java/com/standardnotes/ReceiveSharingIntentHelper.java b/packages/mobile/android/app/src/main/java/com/standardnotes/ReceiveSharingIntentHelper.java
new file mode 100644
index 000000000..6f56414d0
--- /dev/null
+++ b/packages/mobile/android/app/src/main/java/com/standardnotes/ReceiveSharingIntentHelper.java
@@ -0,0 +1,171 @@
+// Adapted from
+// https://github.com/ajith-ab/react-native-receive-sharing-intent
+
+package com.standardnotes;
+
+import android.annotation.SuppressLint;
+import android.app.Application;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.OpenableColumns;
+
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.bridge.WritableNativeMap;
+
+import java.util.ArrayList;
+import java.util.Objects;
+
+public class ReceiveSharingIntentHelper {
+
+ private Context context;
+
+ public ReceiveSharingIntentHelper(Application context) {
+ this.context = context;
+ }
+
+ public void sendFileNames(Context context, Intent intent, Promise promise) {
+ try {
+ String action = intent.getAction();
+ String type = intent.getType();
+ if (type == null) {
+ return;
+ }
+ if (!type.startsWith("text") && (Objects.equals(action, Intent.ACTION_SEND) || Objects.equals(action, Intent.ACTION_SEND_MULTIPLE))) {
+ WritableMap files = getMediaUris(intent, context);
+ if (files == null) return;
+ promise.resolve(files);
+ }
+ else if (type.startsWith("text") && Objects.equals(action, Intent.ACTION_SEND)) {
+ String text = null;
+ String subject = null;
+ try {
+ text = intent.getStringExtra(Intent.EXTRA_TEXT);
+ subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
+ } catch (Exception ignored) {}
+ WritableMap files;
+ if (text == null) {
+ files = getMediaUris(intent, context);
+ if (files == null) return;
+ }
+ else {
+ files = new WritableNativeMap();
+ WritableMap file = new WritableNativeMap();
+ file.putString("contentUri", null);
+ file.putString("fileName", null);
+ file.putString("extension", null);
+ if (text.startsWith("http")) {
+ file.putString("weblink", text);
+ file.putString("text", null);
+ } else {
+ file.putString("weblink", null);
+ file.putString("text", text);
+ }
+ file.putString("subject", subject);
+ files.putMap("0", file);
+ }
+ promise.resolve(files);
+ }
+ else if (Objects.equals(action, Intent.ACTION_VIEW)) {
+ String link = intent.getDataString();
+ WritableMap files = new WritableNativeMap();
+ WritableMap file = new WritableNativeMap();
+ file.putString("contentUri", null);
+ file.putString("mimeType", null);
+ file.putString("text", null);
+ file.putString("weblink", link);
+ file.putString("fileName", null);
+ file.putString("extension", null);
+ files.putMap("0", file);
+ promise.resolve(files);
+ }
+ else if (Objects.equals(action, Intent.ACTION_PROCESS_TEXT)) {
+ String text = null;
+ try {
+ text = intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT);
+ } catch (Exception ignored) {
+ }
+ WritableMap files = new WritableNativeMap();
+ WritableMap file = new WritableNativeMap();
+ file.putString("contentUri", null);
+ file.putString("fileName", null);
+ file.putString("extension", null);
+ file.putString("weblink", null);
+ file.putString("text", text);
+ files.putMap("0", file);
+ promise.resolve(files);
+ }
+ else {
+ promise.reject("error", "Invalid file type.");
+ }
+ } catch (Exception e) {
+ promise.reject("error", e.toString());
+ }
+ }
+
+
+ @SuppressLint("Range")
+ public WritableMap getMediaUris(Intent intent, Context context) {
+ if (intent == null) return null;
+
+ String subject = null;
+ try {
+ subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
+ } catch (Exception ignored) {
+ }
+
+ WritableMap files = new WritableNativeMap();
+ if (Objects.equals(intent.getAction(), Intent.ACTION_SEND)) {
+ WritableMap file = new WritableNativeMap();
+ Uri contentUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
+ if (contentUri == null) return null;
+ ContentResolver contentResolver = context.getContentResolver();
+ file.putString("mimeType", contentResolver.getType(contentUri));
+ Cursor queryResult = contentResolver.query(contentUri, null, null, null, null);
+ queryResult.moveToFirst();
+ file.putString("fileName", queryResult.getString(queryResult.getColumnIndex(OpenableColumns.DISPLAY_NAME)));
+ file.putString("contentUri", contentUri.toString());
+ file.putString("text", null);
+ file.putString("weblink", null);
+ file.putString("subject", subject);
+ files.putMap("0", file);
+ queryResult.close();
+ } else if (Objects.equals(intent.getAction(), Intent.ACTION_SEND_MULTIPLE)) {
+ ArrayList contentUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+ if (contentUris != null) {
+ int index = 0;
+ for (Uri uri : contentUris) {
+ WritableMap file = new WritableNativeMap();
+ ContentResolver contentResolver = context.getContentResolver();
+ // Based on https://developer.android.com/training/secure-file-sharing/retrieve-info
+ file.putString("mimeType", contentResolver.getType(uri));
+ Cursor queryResult = contentResolver.query(uri, null, null, null, null);
+ queryResult.moveToFirst();
+ file.putString("fileName", queryResult.getString(queryResult.getColumnIndex(OpenableColumns.DISPLAY_NAME)));
+ file.putString("contentUri", uri.toString());
+ file.putString("text", null);
+ file.putString("weblink", null);
+ file.putString("subject", subject);
+ files.putMap(Integer.toString(index), file);
+ queryResult.close();
+ index++;
+ }
+ }
+ }
+ return files;
+ }
+
+ public void clearFileNames(Intent intent) {
+ String type = intent.getType();
+ if (type == null) return;
+ if (type.startsWith("text")) {
+ intent.removeExtra(Intent.EXTRA_TEXT);
+ } else if (type.startsWith("image") || type.startsWith("video") || type.startsWith("application")) {
+ intent.removeExtra(Intent.EXTRA_STREAM);
+ }
+ }
+
+}
diff --git a/packages/mobile/android/app/src/main/java/com/standardnotes/ReceiveSharingIntentModule.java b/packages/mobile/android/app/src/main/java/com/standardnotes/ReceiveSharingIntentModule.java
new file mode 100644
index 000000000..99322ea05
--- /dev/null
+++ b/packages/mobile/android/app/src/main/java/com/standardnotes/ReceiveSharingIntentModule.java
@@ -0,0 +1,64 @@
+// Adapted from
+// https://github.com/ajith-ab/react-native-receive-sharing-intent
+
+package com.standardnotes;
+
+import android.app.Activity;
+import android.app.Application;
+import android.content.Intent;
+
+
+import android.os.Build;
+import androidx.annotation.RequiresApi;
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+
+
+public class ReceiveSharingIntentModule extends ReactContextBaseJavaModule {
+ public final String Log_Tag = "ReceiveSharingIntent";
+
+ private final ReactApplicationContext reactContext;
+ private ReceiveSharingIntentHelper receiveSharingIntentHelper;
+
+ public ReceiveSharingIntentModule(ReactApplicationContext reactContext) {
+ super(reactContext);
+ this.reactContext = reactContext;
+ Application applicationContext = (Application) reactContext.getApplicationContext();
+ receiveSharingIntentHelper = new ReceiveSharingIntentHelper(applicationContext);
+ }
+
+
+ protected void onNewIntent(Intent intent) {
+ Activity mActivity = getCurrentActivity();
+ if(mActivity == null) { return; }
+ mActivity.setIntent(intent);
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+ @ReactMethod
+ public void getFileNames(Promise promise){
+ Activity mActivity = getCurrentActivity();
+ if(mActivity == null) { return; }
+ Intent intent = mActivity.getIntent();
+ if(intent == null) { return; }
+ receiveSharingIntentHelper.sendFileNames(reactContext, intent, promise);
+ mActivity.setIntent(null);
+ }
+
+ @ReactMethod
+ public void clearFileNames(){
+ Activity mActivity = getCurrentActivity();
+ if(mActivity == null) { return; }
+ Intent intent = mActivity.getIntent();
+ if(intent == null) { return; }
+ receiveSharingIntentHelper.clearFileNames(intent);
+ }
+
+
+ @Override
+ public String getName() {
+ return "ReceiveSharingIntent";
+ }
+}
diff --git a/packages/mobile/android/app/src/main/java/com/standardnotes/ReceiveSharingIntentPackage.java b/packages/mobile/android/app/src/main/java/com/standardnotes/ReceiveSharingIntentPackage.java
new file mode 100644
index 000000000..15c69dd53
--- /dev/null
+++ b/packages/mobile/android/app/src/main/java/com/standardnotes/ReceiveSharingIntentPackage.java
@@ -0,0 +1,27 @@
+package com.standardnotes;
+
+import com.facebook.react.ReactPackage;
+import com.facebook.react.bridge.NativeModule;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.uimanager.ViewManager;
+import com.standardnotes.ReceiveSharingIntentModule;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class ReceiveSharingIntentPackage implements ReactPackage {
+ @Override
+ public List createViewManagers(ReactApplicationContext reactContext) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List createNativeModules(ReactApplicationContext reactContext) {
+ List modules = new ArrayList<>();
+
+ modules.add(new ReceiveSharingIntentModule(reactContext));
+
+ return modules;
+ }
+}
diff --git a/packages/mobile/src/CustomAndroidWebView.tsx b/packages/mobile/src/CustomAndroidWebView.tsx
new file mode 100644
index 000000000..04ccaa45d
--- /dev/null
+++ b/packages/mobile/src/CustomAndroidWebView.tsx
@@ -0,0 +1,4 @@
+import { requireNativeComponent } from 'react-native'
+import { NativeWebViewAndroid } from 'react-native-webview/lib/WebViewTypes'
+
+export default requireNativeComponent('CustomWebView') as NativeWebViewAndroid
diff --git a/packages/mobile/src/Lib/MobileDevice.ts b/packages/mobile/src/Lib/MobileDevice.ts
index 5bcaf1c50..306995216 100644
--- a/packages/mobile/src/Lib/MobileDevice.ts
+++ b/packages/mobile/src/Lib/MobileDevice.ts
@@ -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 = 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)
}
}
diff --git a/packages/mobile/src/MobileWebAppContainer.tsx b/packages/mobile/src/MobileWebAppContainer.tsx
index 0aefcbacb..cdbedb1bc 100644
--- a/packages/mobile/src/MobileWebAppContainer.tsx
+++ b/packages/mobile/src/MobileWebAppContainer.tsx
@@ -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 (
vo
overScrollMode="never"
nativeConfig={Platform.select({
android: {
- component: CustomWebView,
+ component: CustomAndroidWebView,
},
})}
/>
diff --git a/packages/mobile/src/ReceivedSharedItemsHandler.ts b/packages/mobile/src/ReceivedSharedItemsHandler.ts
new file mode 100644
index 000000000..2b4a9afda
--- /dev/null
+++ b/packages/mobile/src/ReceivedSharedItemsHandler.ts
@@ -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) {
+ 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) => {
+ 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)
+ }
+}
diff --git a/packages/services/src/Domain/Device/MobileDeviceInterface.ts b/packages/services/src/Domain/Device/MobileDeviceInterface.ts
index 022435cdf..e96254b54 100644
--- a/packages/services/src/Domain/Device/MobileDeviceInterface.ts
+++ b/packages/services/src/Domain/Device/MobileDeviceInterface.ts
@@ -3,6 +3,7 @@ import { Environment, Platform, RawKeychainValue } from '@standardnotes/models'
import { AppleIAPProductId } from './../Subscription/AppleIAPProductId'
import { DeviceInterface } from './DeviceInterface'
import { AppleIAPReceipt } from '../Subscription/AppleIAPReceipt'
+import { ApplicationEvent } from '../Event/ApplicationEvent'
export interface MobileDeviceInterface extends DeviceInterface {
environment: Environment.Mobile
@@ -27,4 +28,5 @@ export interface MobileDeviceInterface extends DeviceInterface {
getColorScheme(): Promise<'light' | 'dark' | null | undefined>
purchaseSubscriptionIAP(plan: AppleIAPProductId): Promise
authenticateWithU2F(authenticationOptionsJSONString: string): Promise | null>
+ notifyApplicationEvent(event: ApplicationEvent): void
}
diff --git a/packages/snjs/lib/Client/ReactNativeToWebEvent.ts b/packages/snjs/lib/Client/ReactNativeToWebEvent.ts
index a67454003..c232859ee 100644
--- a/packages/snjs/lib/Client/ReactNativeToWebEvent.ts
+++ b/packages/snjs/lib/Client/ReactNativeToWebEvent.ts
@@ -9,4 +9,6 @@ export enum ReactNativeToWebEvent {
KeyboardFrameDidChange = 'KeyboardFrameDidChange',
KeyboardWillShow = 'KeyboardWillShow',
KeyboardWillHide = 'KeyboardWillHide',
+ ReceivedFile = 'ReceivedFile',
+ ReceivedText = 'ReceivedText',
}
diff --git a/packages/ui-services/src/WebApplication/WebApplicationInterface.ts b/packages/ui-services/src/WebApplication/WebApplicationInterface.ts
index 79f7a8aac..dfb128f14 100644
--- a/packages/ui-services/src/WebApplication/WebApplicationInterface.ts
+++ b/packages/ui-services/src/WebApplication/WebApplicationInterface.ts
@@ -15,6 +15,8 @@ export interface WebApplicationInterface extends ApplicationInterface {
handleMobileColorSchemeChangeEvent(): void
handleMobileKeyboardWillChangeFrameEvent(frame: { height: number; contentHeight: number }): void
handleMobileKeyboardDidChangeFrameEvent(frame: { height: number; contentHeight: number }): void
+ handleReceivedFileEvent(file: { name: string; mimeType: string; data: string }): void
+ handleReceivedTextEvent(item: { text: string; title?: string }): Promise
isNativeMobileWeb(): boolean
mobileDevice(): MobileDeviceInterface
handleAndroidBackButtonPressed(): void
diff --git a/packages/web/src/javascripts/Application/WebApplication.ts b/packages/web/src/javascripts/Application/WebApplication.ts
index c070f9cd1..479a9a939 100644
--- a/packages/web/src/javascripts/Application/WebApplication.ts
+++ b/packages/web/src/javascripts/Application/WebApplication.ts
@@ -24,11 +24,13 @@ import {
BackupServiceInterface,
InternalFeatureService,
InternalFeatureServiceInterface,
+ NoteContent,
+ SNNote,
} from '@standardnotes/snjs'
import { makeObservable, observable } from 'mobx'
import { startAuthentication, startRegistration } from '@simplewebauthn/browser'
import { PanelResizedData } from '@/Types/PanelResizedData'
-import { isAndroid, isDesktopApplication, isDev, isIOS } from '@/Utils'
+import { getBlobFromBase64, isAndroid, isDesktopApplication, isDev, isIOS } from '@/Utils'
import { DesktopManager } from './Device/DesktopManager'
import {
ArchiveManager,
@@ -66,6 +68,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
private readonly mobileWebReceiver?: MobileWebReceiver
private readonly androidBackHandler?: AndroidBackHandler
private readonly visibilityObserver?: VisibilityObserver
+ private readonly mobileAppEventObserver?: () => void
public readonly devMode?: DevMode
@@ -137,6 +140,9 @@ export class WebApplication extends SNApplication implements WebApplicationInter
if (this.isNativeMobileWeb()) {
this.mobileWebReceiver = new MobileWebReceiver(this)
this.androidBackHandler = new AndroidBackHandler()
+ this.mobileAppEventObserver = this.addEventObserver(async (event) => {
+ this.mobileDevice().notifyApplicationEvent(event)
+ })
// eslint-disable-next-line no-console
console.log = (...args) => {
@@ -186,6 +192,11 @@ export class WebApplication extends SNApplication implements WebApplicationInter
this.visibilityObserver.deinit()
;(this.visibilityObserver as unknown) = undefined
}
+
+ if (this.mobileAppEventObserver) {
+ this.mobileAppEventObserver()
+ ;(this.mobileAppEventObserver as unknown) = undefined
+ }
} catch (error) {
console.error('Error while deiniting application', error)
}
@@ -376,6 +387,27 @@ export class WebApplication extends SNApplication implements WebApplicationInter
this.notifyWebEvent(WebAppEvent.MobileKeyboardDidChangeFrame, frame)
}
+ handleReceivedFileEvent(file: { name: string; mimeType: string; data: string }): void {
+ const filesController = this.getViewControllerManager().filesController
+ const blob = getBlobFromBase64(file.data, file.mimeType)
+ const mappedFile = new File([blob], file.name, { type: file.mimeType })
+ void filesController.uploadNewFile(mappedFile, true)
+ }
+
+ async handleReceivedTextEvent({ text, title }: { text: string; title?: string | undefined }) {
+ const titleForNote = title || this.getViewControllerManager().itemListController.titleForNewNote()
+
+ const note = this.items.createTemplateItem(ContentType.Note, {
+ title: titleForNote,
+ text: text,
+ references: [],
+ })
+
+ const insertedNote = await this.mutator.insertItem(note)
+
+ this.getViewControllerManager().selectionController.selectItem(insertedNote.uuid, true).catch(console.error)
+ }
+
private async lockApplicationAfterMobileEventIfApplicable(): Promise {
const isLocked = await this.isLocked()
if (isLocked) {
diff --git a/packages/web/src/javascripts/NativeMobileWeb/MobileWebReceiver.ts b/packages/web/src/javascripts/NativeMobileWeb/MobileWebReceiver.ts
index ef90369dd..0e26d6e2a 100644
--- a/packages/web/src/javascripts/NativeMobileWeb/MobileWebReceiver.ts
+++ b/packages/web/src/javascripts/NativeMobileWeb/MobileWebReceiver.ts
@@ -83,6 +83,18 @@ export class MobileWebReceiver {
messageData as { height: number; contentHeight: number },
)
break
+ case ReactNativeToWebEvent.ReceivedFile:
+ void this.application.handleReceivedFileEvent(
+ messageData as {
+ name: string
+ mimeType: string
+ data: string
+ },
+ )
+ break
+ case ReactNativeToWebEvent.ReceivedText:
+ void this.application.handleReceivedTextEvent(messageData as { text: string; title?: string })
+ break
default:
break
diff --git a/packages/web/src/javascripts/Utils/Utils.ts b/packages/web/src/javascripts/Utils/Utils.ts
index de9b30374..e54528e46 100644
--- a/packages/web/src/javascripts/Utils/Utils.ts
+++ b/packages/web/src/javascripts/Utils/Utils.ts
@@ -229,3 +229,23 @@ export const getBase64FromBlob = (blob: Blob) => {
reader.readAsDataURL(blob)
})
}
+
+export const getBlobFromBase64 = (b64Data: string, contentType = '', sliceSize = 512) => {
+ const byteCharacters = atob(b64Data)
+ const byteArrays = []
+
+ for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
+ const slice = byteCharacters.slice(offset, offset + sliceSize)
+
+ const byteNumbers = new Array(slice.length)
+ for (let i = 0; i < slice.length; i++) {
+ byteNumbers[i] = slice.charCodeAt(i)
+ }
+
+ const byteArray = new Uint8Array(byteNumbers)
+ byteArrays.push(byteArray)
+ }
+
+ const blob = new Blob(byteArrays, { type: contentType })
+ return blob
+}