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() }); }