Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mobile: Resolves #8639: implement callback url #9803

Open
wants to merge 11 commits into
base: dev
Choose a base branch
from
Open
9 changes: 9 additions & 0 deletions packages/app-mobile/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,15 @@
<data android:mimeType="*/*" />
</intent-filter>
<!-- /SHARE EXTENSION -->

<!-- EXTERNAL LINK -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="joplin" />
</intent-filter>
<!-- /EXTERNAL LINK -->
</activity>

</application>
Expand Down
12 changes: 12 additions & 0 deletions packages/app-mobile/components/screens/Note.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -1081,6 +1082,11 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> 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 [];
Expand Down Expand Up @@ -1291,6 +1297,12 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
this.copyMarkdownLink_onPress();
},
});
output.push({
title: _('Copy external link'),
onPress: () => {
this.copyExternalLink_onPress();
},
});
}

output.push({
Expand Down
70 changes: 67 additions & 3 deletions packages/app-mobile/components/screens/Notes.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const React = require('react');
import { AppState as RNAppState, View, StyleSheet, NativeEventSubscription } from 'react-native';
import { AppState as RNAppState, View, StyleSheet, NativeEventSubscription, Alert } from 'react-native';
import { stateUtils } from '@joplin/lib/reducer';
import { connect } from 'react-redux';
import NoteList from '../NoteList';
Expand All @@ -8,7 +8,7 @@ import Tag from '@joplin/lib/models/Tag';
import Note from '@joplin/lib/models/Note';
import Setting from '@joplin/lib/models/Setting';
import { themeStyle } from '../global-style';
import { ScreenHeader } from '../ScreenHeader';
import { ScreenHeader, MenuOptionType } from '../ScreenHeader';
import { _ } from '@joplin/lib/locale';
import ActionButton from '../ActionButton';
const { dialogs } = require('../../utils/dialogs.js');
Expand All @@ -19,6 +19,8 @@ import { AppState } from '../../utils/types';
import { NoteEntity } from '@joplin/lib/services/database/types';
import { itemIsInTrash } from '@joplin/lib/services/trash';
const { ALL_NOTES_FILTER_ID } = require('@joplin/lib/reserved-ids.js');
const Clipboard = require('@react-native-community/clipboard').default;
import { getFolderCallbackUrl, getTagCallbackUrl } from '@joplin/lib/callbackUrlUtils';

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
class NotesScreenComponent extends BaseScreenComponent<any> {
Expand Down Expand Up @@ -202,6 +204,67 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
return this.folderPickerOptions_;
}

public menuOptions() {
const output: MenuOptionType[] = [];

if (this.props.notesParentType === 'Tag') {
output.push({
title: _('Copy external link'),
onPress: () => {
Clipboard.setString(getTagCallbackUrl(this.props.selectedTagId));
},
});
}
if (this.props.notesParentType === 'Folder') {
output.push({
title: _('Copy external link'),
onPress: () => {
Clipboard.setString(getFolderCallbackUrl(this.props.selectedFolderId));
},
});
// menu items originally in long-pressing Notebook items in side menu
// app-mobile/components/side-menu-content.tsx
output.push({
title: _('Edit notebook'),
onPress: () => {
this.props.dispatch({
type: 'NAV_GO',
routeName: 'Folder',
folderId: this.props.selectedFolderId,
});
},
});
output.push({
title: _('Delete notebook'),
onPress: () => {
const folderDeletion = (message: string) => {
Alert.alert('', message, [
{
text: _('OK'),
onPress: () => {
void Folder.delete(this.props.selectedFolderId);
},
},
{
text: _('Cancel'),
onPress: () => { },
style: 'cancel',
},
]);
};
if (this.props.selectedFolderId === this.props.inboxJopId) {
return folderDeletion(
_('Delete the Inbox notebook?\n\nIf you delete the inbox notebook, any email that\'s recently been sent to it may be lost.'),
);
}
return folderDeletion(_('Delete notebook "%s"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.', this.parentItem().title));
},
});
}

return output;
}

public render() {
const parent = this.parentItem();
const theme = themeStyle(this.props.themeId);
Expand Down Expand Up @@ -292,7 +355,7 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
accessibilityElementsHidden={accessibilityHidden}
importantForAccessibility={accessibilityHidden ? 'no-hide-descendants' : undefined}
>
<ScreenHeader title={iconString + title} showBackButton={false} parentComponent={thisComp} sortButton_press={this.sortButton_press} folderPickerOptions={this.folderPickerOptions()} showSearchButton={true} showSideMenuButton={true} />
<ScreenHeader title={iconString + title} showBackButton={false} parentComponent={thisComp} sortButton_press={this.sortButton_press} folderPickerOptions={this.folderPickerOptions()} showSearchButton={true} showSideMenuButton={true} menuOptions={this.menuOptions()} />
<NoteList />
{actionButtonComp}
<DialogBox
Expand Down Expand Up @@ -323,6 +386,7 @@ const NotesScreen = connect((state: AppState) => {
themeId: state.settings.theme,
noteSelectionEnabled: state.noteSelectionEnabled,
notesOrder: stateUtils.notesOrder(state.settings),
inboxJopId: state.settings['sync.10.inboxId'],
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
})(NotesScreenComponent as any);
Expand Down
54 changes: 54 additions & 0 deletions packages/app-mobile/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -811,6 +812,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<any>(res => res);
private callbackUrl: string|null = null;

public constructor() {
super();
Expand Down Expand Up @@ -841,6 +843,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();
}
}
};

Expand Down Expand Up @@ -1001,6 +1009,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();
}
}

Expand Down Expand Up @@ -1045,6 +1054,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() });
}
Expand Down