diff --git a/packages/app-mobile/android/app/src/main/AndroidManifest.xml b/packages/app-mobile/android/app/src/main/AndroidManifest.xml
index 3972acaf8a3..60022c03124 100644
--- a/packages/app-mobile/android/app/src/main/AndroidManifest.xml
+++ b/packages/app-mobile/android/app/src/main/AndroidManifest.xml
@@ -92,6 +92,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/app-mobile/components/screens/Note.tsx b/packages/app-mobile/components/screens/Note.tsx
index 66b29f720a7..dc0fddd88a0 100644
--- a/packages/app-mobile/components/screens/Note.tsx
+++ b/packages/app-mobile/components/screens/Note.tsx
@@ -55,6 +55,7 @@ import { join } from 'path';
import { Dispatch } from 'redux';
import { RefObject } from 'react';
import { SelectionRange } from '../NoteEditor/types';
+import { getNoteCallbackUrl } from '@joplin/lib/callbackUrlUtils';
import { AppState } from '../../utils/types';
import restoreItems from '@joplin/lib/services/trash/restoreItems';
import { getDisplayParentTitle } from '@joplin/lib/services/trash';
@@ -1083,6 +1084,11 @@ class NoteScreenComponent extends BaseScreenComponent implements B
Clipboard.setString(Note.markdownTag(note));
}
+ private copyExternalLink_onPress() {
+ const note = this.state.note;
+ Clipboard.setString(getNoteCallbackUrl(note.id));
+ }
+
public sideMenuOptions() {
const note = this.state.note;
if (!note) return [];
@@ -1295,6 +1301,12 @@ class NoteScreenComponent extends BaseScreenComponent implements B
this.copyMarkdownLink_onPress();
},
});
+ output.push({
+ title: _('Copy external link'),
+ onPress: () => {
+ this.copyExternalLink_onPress();
+ },
+ });
}
output.push({
diff --git a/packages/app-mobile/root.tsx b/packages/app-mobile/root.tsx
index 2b5f715c82f..aba37f72358 100644
--- a/packages/app-mobile/root.tsx
+++ b/packages/app-mobile/root.tsx
@@ -85,6 +85,7 @@ const SyncTargetDropbox = require('@joplin/lib/SyncTargetDropbox.js');
const SyncTargetAmazonS3 = require('@joplin/lib/SyncTargetAmazonS3.js');
import BiometricPopup from './components/biometrics/BiometricPopup';
import initLib from '@joplin/lib/initLib';
+import { isCallbackUrl, parseCallbackUrl, CallbackUrlCommand } from '@joplin/lib/callbackUrlUtils';
import JoplinCloudLoginScreen from './components/screens/JoplinCloudLoginScreen';
SyncTargetRegistry.addClass(SyncTargetNone);
@@ -826,6 +827,7 @@ class AppComponent extends React.Component {
private themeChangeListener_: NativeEventSubscription|null = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private dropdownAlert_ = (_data: any) => new Promise(res => res);
+ private callbackUrl: string|null = null;
public constructor() {
super();
@@ -856,6 +858,12 @@ class AppComponent extends React.Component {
if (event.url === ShareExtension.shareURL && this.props.biometricsDone) {
logger.info('Sharing: handleOpenURL_: Processing share data');
void this.handleShareData();
+ } else if (isCallbackUrl(event.url)) {
+ logger.info('received callback url: ', event.url);
+ this.callbackUrl = event.url;
+ if (this.props.biometricsDone) {
+ void this.handleCallbackUrl();
+ }
}
};
@@ -1016,6 +1024,7 @@ class AppComponent extends React.Component {
if (this.props.biometricsDone !== prevProps.biometricsDone && this.props.biometricsDone) {
logger.info('Sharing: componentDidUpdate: biometricsDone');
void this.handleShareData();
+ void this.handleCallbackUrl();
}
}
@@ -1060,6 +1069,51 @@ class AppComponent extends React.Component {
}
}
+ private async handleCallbackUrl() {
+ const url = this.callbackUrl;
+ this.callbackUrl = null;
+ if (url === null) {
+ return;
+ }
+
+ const { command, params } = parseCallbackUrl(url);
+
+ // adopted from app-mobile/utils/shareHandler.ts
+ // We go back one screen in case there's already a note open -
+ // if we don't do this, the dispatch below will do nothing
+ // (because routeName wouldn't change)
+ this.props.dispatch({ type: 'NAV_BACK' });
+ this.props.dispatch({ type: 'SIDE_MENU_CLOSE' });
+
+ switch (command) {
+
+ case CallbackUrlCommand.OpenNote:
+ this.props.dispatch({
+ type: 'NAV_GO',
+ routeName: 'Note',
+ noteId: params.id,
+ });
+ break;
+
+ case CallbackUrlCommand.OpenTag:
+ this.props.dispatch({
+ type: 'NAV_GO',
+ routeName: 'Notes',
+ tagId: params.id,
+ });
+ break;
+
+ case CallbackUrlCommand.OpenFolder:
+ this.props.dispatch({
+ type: 'NAV_GO',
+ routeName: 'Notes',
+ folderId: params.id,
+ });
+ break;
+
+ }
+ }
+
private async handleScreenWidthChange_() {
this.setState({ sideMenuWidth: this.getSideMenuWidth() });
}