diff --git a/.eslintignore b/.eslintignore
index 58fd3b195..069b17363 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -45,4 +45,8 @@ Server/docs/
Server/dist/
Server/bin/
Server/node_modules/
-ElectronClient/app/packageInfo.js
\ No newline at end of file
+ElectronClient/app/packageInfo.js
+
+# Ignore files generated from TypeScript files
+ElectronClient/app/gui/ShareNoteDialog.js
+ReactNativeClient/lib/JoplinServerApi.js
diff --git a/.gitignore b/.gitignore
index 14940ee97..f930ed530 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,3 +44,8 @@ ElectronClient/app/gui/note-viewer/fonts/
ElectronClient/app/gui/note-viewer/lib.js
Tools/commit_hook.txt
.vscode/*
+*.map
+
+# Ignore files generated from TypeScript files
+ElectronClient/app/gui/ShareNoteDialog.js
+ReactNativeClient/lib/JoplinServerApi.js
diff --git a/.travis.yml b/.travis.yml
index 7ed9ff2a0..e56c2b32a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -55,8 +55,12 @@ before_install:
script:
- |
+ # Copy lib
+ rsync -aP --delete ReactNativeClient/lib/ ElectronClient/app/lib/
+
# Install tools
npm install
+ npm run typescript-compile
cd Tools
npm install
cd ..
@@ -110,5 +114,4 @@ script:
# Prepare the Electron app and build it
cd ElectronClient/app
- rsync -aP --delete ../../ReactNativeClient/lib/ lib/
npm install && USE_HARD_LINKS=false yarn dist
diff --git a/BUILD.md b/BUILD.md
index 46cbb12c1..7e00b45e9 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -5,6 +5,14 @@
- All the applications share the same library, which, for historical reasons, is in ReactNativeClient/lib. This library is copied to the relevant directories when building each app.
- In general, most of the backend (anything to do with the database, synchronisation, data import or export, etc.) is shared across all the apps, so when making a change please consider how it will affect all the apps.
+# TypeScript
+
+Most of the application is written in JavaScript, however new classes and files should generally be written in [TypeScript](https://www.typescriptlang.org/). Even if you don't write TypeScript code, you will need to build the existing .ts and .tsx files. This is done from the root of the project, by running `npm run typescript-compile`.
+
+If you are modifying TypeScript code, the best is to have the compiler watch for changes from a terminal. To do so, run `npm run typescript-watch`.
+
+All TypeScript files are generated next to the .ts or .tsx file. So for example, if there's a file "lib/MyClass.ts", there will be a generated "lib/MyClass.js" next to it. If you create a new TypeScript file, make sure you add the generated .js file to .gitignore. It is implemented that way as it requires minimal changes to integrate TypeScript in the existing JavaScript code base.
+
## macOS dependencies
brew install yarn node
@@ -14,7 +22,7 @@
## Linux and Windows (WSL) dependencies
- Install yarn - https://yarnpkg.com/lang/en/docs/install/
-- Install node v8.x (check with `node --version`) - https://nodejs.org/en/
+- Install node v10.x (check with `node --version`) - https://nodejs.org/en/
- If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`
# Building the tools
@@ -28,8 +36,9 @@ npm install && cd Tools && npm install
# Building the Electron application
```
+rsync --delete -a ReactNativeClient/lib/ ElectronClient/app/lib/
+npm run typescript-compile
cd ElectronClient/app
-rsync --delete -a ../../ReactNativeClient/lib/ lib/
npm install
yarn dist
```
@@ -47,10 +56,9 @@ From `/ElectronClient` you can also run `run.sh` to run the app for testing.
## Building Electron application on Windows
```
-cd Tools
-npm install
-cd ..\ElectronClient\app
-xcopy /C /I /H /R /Y /S ..\..\ReactNativeClient\lib lib
+xcopy /C /I /H /R /Y /S ReactNativeClient\lib ElectronClient\app\lib
+npm run typescript-compile
+cd ElectronClient\app
npm install
yarn dist
```
@@ -67,7 +75,15 @@ The [building\_win32\_tips on this page](./readme/building_win32_tips.md) might
First you need to setup React Native to build projects with native code. For this, follow the instructions on the [Get Started](https://facebook.github.io/react-native/docs/getting-started.html) tutorial, in the "React Native CLI Quickstart" tab.
-Then, from `/ReactNativeClient`, run `npm install`, then `react-native run-ios` or `react-native run-android`.
+Then:
+
+```
+npm run typescript-compile
+cd ReactNativeClient
+npm install
+react-native run-ios
+# Or: react-native run-android
+```
# Building the Terminal application
@@ -75,7 +91,6 @@ Then, from `/ReactNativeClient`, run `npm install`, then `react-native run-ios`
cd CliClient
npm install
./build.sh
-rsync --delete -aP ../ReactNativeClient/locales/ build/locales/
```
Run `run.sh` to start the application for testing.
diff --git a/CliClient/build.sh b/CliClient/build.sh
index 38c49293a..e7190706c 100755
--- a/CliClient/build.sh
+++ b/CliClient/build.sh
@@ -7,4 +7,9 @@ rsync -a --exclude "node_modules/" "$ROOT_DIR/app/" "$BUILD_DIR/"
rsync -a --delete "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
rsync -a --delete "$ROOT_DIR/../ReactNativeClient/locales/" "$BUILD_DIR/locales/"
cp "$ROOT_DIR/package.json" "$BUILD_DIR"
+
+cd $ROOT_DIR/..
+npm run typescript-compile
+cd $ROOT_DIR
+
chmod 755 "$BUILD_DIR/main.js"
\ No newline at end of file
diff --git a/ElectronClient/app/gui/ConfigScreen.jsx b/ElectronClient/app/gui/ConfigScreen.jsx
index a7db35001..38bf1eb21 100644
--- a/ElectronClient/app/gui/ConfigScreen.jsx
+++ b/ElectronClient/app/gui/ConfigScreen.jsx
@@ -24,6 +24,19 @@ class ConfigScreenComponent extends React.Component {
await shared.checkSyncConfig(this, this.state.settings);
};
+ this.checkNextcloudAppButton_click = async () => {
+ this.setState({ showNextcloudAppLog: true });
+ await shared.checkNextcloudApp(this, this.state.settings);
+ };
+
+ this.showLogButton_click = () => {
+ this.setState({ showNextcloudAppLog: true });
+ };
+
+ this.nextcloudAppHelpLink_click = () => {
+ bridge().openExternal('https://joplinapp.org/nextcloud_app');
+ };
+
this.rowStyle_ = {
marginBottom: 10,
};
@@ -31,7 +44,7 @@ class ConfigScreenComponent extends React.Component {
this.configMenuBar_selectionChange = this.configMenuBar_selectionChange.bind(this);
}
- componentWillMount() {
+ UNSAFE_componentWillMount() {
this.setState({ settings: this.props.settings });
}
@@ -93,14 +106,21 @@ class ConfigScreenComponent extends React.Component {
sectionToComponent(key, section, settings, selected) {
const theme = themeStyle(this.props.theme);
- const settingComps = [];
+ // const settingComps = [];
- for (let i = 0; i < section.metadatas.length; i++) {
- const md = section.metadatas[i];
+ const createSettingComponents = (advanced) => {
+ const output = [];
+ for (let i = 0; i < section.metadatas.length; i++) {
+ const md = section.metadatas[i];
+ if (!!md.advanced !== advanced) continue;
+ const settingComp = this.settingToComponent(md.key, settings[md.key]);
+ output.push(settingComp);
+ }
+ return output;
+ };
- const settingComp = this.settingToComponent(md.key, settings[md.key]);
- settingComps.push(settingComp);
- }
+ const settingComps = createSettingComponents(false);
+ const advancedSettingComps = createSettingComponents(true);
const sectionStyle = {
marginTop: 20,
@@ -117,10 +137,10 @@ class ConfigScreenComponent extends React.Component {
if (section.name === 'sync') {
const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']);
+ const statusStyle = Object.assign({}, theme.textStyle, { marginTop: 10 });
if (syncTargetMd.supportsConfigCheck) {
const messages = shared.checkSyncConfigMessages(this);
- const statusStyle = Object.assign({}, theme.textStyle, { marginTop: 10 });
const statusComp = !messages.length ? null : (
{messages[0]}
@@ -137,12 +157,69 @@ class ConfigScreenComponent extends React.Component {
);
}
+
+ if (syncTargetMd.name === 'nextcloud') {
+ const syncTarget = settings['sync.5.syncTargets'][settings['sync.5.path']];
+
+ let status = _('Unknown');
+ let errorMessage = null;
+
+ if (this.state.checkNextcloudAppResult === 'checking') {
+ status = _('Checking...');
+ } else if (syncTarget) {
+ if (syncTarget.uuid) status = _('OK');
+ if (syncTarget.error) {
+ status = _('Error');
+ errorMessage = syncTarget.error;
+ }
+ }
+
+ const statusComp = !errorMessage || this.state.checkNextcloudAppResult === 'checking' || !this.state.showNextcloudAppLog ? null : (
+
+
{_('The Joplin Nextcloud App is either not installed or misconfigured. Please see the full error message below:')}
+
{errorMessage}
+
+ );
+
+ const showLogButton = !errorMessage || this.state.showNextcloudAppLog ? null : (
+ [{_('Show Log')}]
+ );
+
+ const appStatusStyle = Object.assign({}, theme.textStyle, { fontWeight: 'bold' });
+
+ settingComps.push(
+
+
Beta: {_('Joplin Nextcloud App status:')} {status}
+
+ {showLogButton}
+
+
+ {_('Check Status')}
+
+
+
[{_('Help')}]
+ {statusComp}
+
+ );
+ }
+ }
+
+ let advancedSettingsButton = null;
+ let advancedSettingsSectionStyle = { display: 'none' };
+
+ if (advancedSettingComps.length) {
+ const iconName = this.state.showAdvancedSettings ? 'fa fa-toggle-up' : 'fa fa-toggle-down';
+ const advancedSettingsButtonStyle = Object.assign({}, theme.buttonStyle, { marginBottom: 10 });
+ advancedSettingsButton = shared.advancedSettingsButton_click(this)} style={advancedSettingsButtonStyle}> {_('Show Advanced Settings')} ;
+ advancedSettingsSectionStyle.display = this.state.showAdvancedSettings ? 'block' : 'none';
}
return (
{noteComp}
{settingComps}
+ {advancedSettingsButton}
+
{advancedSettingComps}
);
}
diff --git a/ElectronClient/app/gui/DialogButtonRow.jsx b/ElectronClient/app/gui/DialogButtonRow.jsx
new file mode 100644
index 000000000..e4bd2cf09
--- /dev/null
+++ b/ElectronClient/app/gui/DialogButtonRow.jsx
@@ -0,0 +1,45 @@
+const React = require('react');
+const { _ } = require('lib/locale.js');
+const { themeStyle } = require('../theme.js');
+
+function DialogButtonRow(props) {
+ const theme = themeStyle(props.theme);
+
+ const okButton_click = () => {
+ if (props.onClick) props.onClick({ buttonName: 'ok' });
+ };
+
+ const cancelButton_click = () => {
+ if (props.onClick) props.onClick({ buttonName: 'cancel' });
+ };
+
+ const onKeyDown = (event) => {
+ if (event.keyCode === 13) {
+ okButton_click();
+ } else if (event.keyCode === 27) {
+ cancelButton_click();
+ }
+ };
+
+ const buttonComps = [];
+
+ if (props.okButtonShow !== false) {
+ buttonComps.push(
+
+ {_('OK')}
+
+ );
+ }
+
+ if (props.cancelButtonShow !== false) {
+ buttonComps.push(
+
+ {props.cancelButtonLabel ? props.cancelButtonLabel : _('Cancel')}
+
+ );
+ }
+
+ return {buttonComps}
;
+}
+
+module.exports = DialogButtonRow;
diff --git a/ElectronClient/app/gui/DropboxLoginScreen.jsx b/ElectronClient/app/gui/DropboxLoginScreen.jsx
index 18b8cf761..88be59f61 100644
--- a/ElectronClient/app/gui/DropboxLoginScreen.jsx
+++ b/ElectronClient/app/gui/DropboxLoginScreen.jsx
@@ -13,7 +13,7 @@ class DropboxLoginScreenComponent extends React.Component {
this.shared_ = new Shared(this, msg => bridge().showInfoMessageBox(msg), msg => bridge().showErrorMessageBox(msg));
}
- componentWillMount() {
+ UNSAFE_componentWillMount() {
this.shared_.refreshUrl();
}
diff --git a/ElectronClient/app/gui/EncryptionConfigScreen.jsx b/ElectronClient/app/gui/EncryptionConfigScreen.jsx
index 51ac9dd9d..0ae093f56 100644
--- a/ElectronClient/app/gui/EncryptionConfigScreen.jsx
+++ b/ElectronClient/app/gui/EncryptionConfigScreen.jsx
@@ -31,11 +31,11 @@ class EncryptionConfigScreenComponent extends React.Component {
return shared.refreshStats(this);
}
- componentWillMount() {
+ UNSAFE_componentWillMount() {
this.initState(this.props);
}
- componentWillReceiveProps(nextProps) {
+ UNSAFE_componentWillReceiveProps(nextProps) {
this.initState(nextProps);
}
diff --git a/ElectronClient/app/gui/Header.jsx b/ElectronClient/app/gui/Header.jsx
index e495d9c32..24bd1d3e2 100644
--- a/ElectronClient/app/gui/Header.jsx
+++ b/ElectronClient/app/gui/Header.jsx
@@ -71,7 +71,7 @@ class HeaderComponent extends React.Component {
};
}
- async componentWillReceiveProps(nextProps) {
+ async UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.windowCommand) {
this.doCommand(nextProps.windowCommand);
}
diff --git a/ElectronClient/app/gui/ImportScreen.jsx b/ElectronClient/app/gui/ImportScreen.jsx
index 6bd2a8a4c..6a46c0f4d 100644
--- a/ElectronClient/app/gui/ImportScreen.jsx
+++ b/ElectronClient/app/gui/ImportScreen.jsx
@@ -8,7 +8,7 @@ const { filename, basename } = require('lib/path-utils.js');
const { importEnex } = require('lib/import-enex');
class ImportScreenComponent extends React.Component {
- componentWillMount() {
+ UNSAFE_componentWillMount() {
this.setState({
doImport: true,
filePath: this.props.filePath,
@@ -16,7 +16,7 @@ class ImportScreenComponent extends React.Component {
});
}
- componentWillReceiveProps(newProps) {
+ UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.filePath) {
this.setState(
{
diff --git a/ElectronClient/app/gui/ItemList.jsx b/ElectronClient/app/gui/ItemList.jsx
index 297ada294..80671ee29 100644
--- a/ElectronClient/app/gui/ItemList.jsx
+++ b/ElectronClient/app/gui/ItemList.jsx
@@ -32,11 +32,11 @@ class ItemList extends React.Component {
});
}
- componentWillMount() {
+ UNSAFE_componentWillMount() {
this.updateStateItemIndexes();
}
- componentWillReceiveProps(newProps) {
+ UNSAFE_componentWillReceiveProps(newProps) {
this.updateStateItemIndexes(newProps);
}
diff --git a/ElectronClient/app/gui/MainScreen.jsx b/ElectronClient/app/gui/MainScreen.jsx
index f8fcd5aa5..627422074 100644
--- a/ElectronClient/app/gui/MainScreen.jsx
+++ b/ElectronClient/app/gui/MainScreen.jsx
@@ -6,6 +6,7 @@ const { NoteList } = require('./NoteList.min.js');
const { NoteText } = require('./NoteText.min.js');
const { PromptDialog } = require('./PromptDialog.min.js');
const NotePropertiesDialog = require('./NotePropertiesDialog.min.js');
+const ShareNoteDialog = require('./ShareNoteDialog.js').default;
const Setting = require('lib/models/Setting.js');
const BaseModel = require('lib/BaseModel.js');
const Tag = require('lib/models/Tag.js');
@@ -24,6 +25,7 @@ class MainScreenComponent extends React.Component {
super();
this.notePropertiesDialog_close = this.notePropertiesDialog_close.bind(this);
+ this.shareNoteDialog_close = this.shareNoteDialog_close.bind(this);
this.sidebar_onDrag = this.sidebar_onDrag.bind(this);
this.noteList_onDrag = this.noteList_onDrag.bind(this);
}
@@ -40,7 +42,11 @@ class MainScreenComponent extends React.Component {
this.setState({ notePropertiesDialogOptions: {} });
}
- componentWillMount() {
+ shareNoteDialog_close() {
+ this.setState({ shareNoteDialogOptions: {} });
+ }
+
+ UNSAFE_componentWillMount() {
this.setState({
promptOptions: null,
modalLayer: {
@@ -48,10 +54,11 @@ class MainScreenComponent extends React.Component {
message: '',
},
notePropertiesDialogOptions: {},
+ shareNoteDialogOptions: {},
});
}
- componentWillReceiveProps(newProps) {
+ UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.windowCommand) {
this.doCommand(newProps.windowCommand);
}
@@ -247,6 +254,13 @@ class MainScreenComponent extends React.Component {
onRevisionLinkClick: command.onRevisionLinkClick,
},
});
+ } else if (command.name === 'commandShareNoteDialog') {
+ this.setState({
+ shareNoteDialogOptions: {
+ noteIds: command.noteIds,
+ visible: true,
+ },
+ });
} else if (command.name === 'toggleVisiblePanes') {
this.toggleVisiblePanes();
} else if (command.name === 'toggleSidebar') {
@@ -573,6 +587,7 @@ class MainScreenComponent extends React.Component {
const modalLayerStyle = Object.assign({}, styles.modalLayer, { display: this.state.modalLayer.visible ? 'block' : 'none' });
const notePropertiesDialogOptions = this.state.notePropertiesDialogOptions;
+ const shareNoteDialogOptions = this.state.shareNoteDialogOptions;
const keyboardMode = Setting.value('editor.keyboardMode');
return (
@@ -580,6 +595,7 @@ class MainScreenComponent extends React.Component {
{this.state.modalLayer.message}
{notePropertiesDialogOptions.visible && }
+ {shareNoteDialogOptions.visible && }
diff --git a/ElectronClient/app/gui/Navigator.jsx b/ElectronClient/app/gui/Navigator.jsx
index abc3b4681..d87fccaa7 100644
--- a/ElectronClient/app/gui/Navigator.jsx
+++ b/ElectronClient/app/gui/Navigator.jsx
@@ -4,7 +4,7 @@ const { connect } = require('react-redux');
const { bridge } = require('electron').remote.require('./bridge');
class NavigatorComponent extends Component {
- componentWillReceiveProps(newProps) {
+ UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.route) {
const screenInfo = this.props.screens[newProps.route.routeName];
let windowTitle = ['Joplin'];
diff --git a/ElectronClient/app/gui/NotePropertiesDialog.jsx b/ElectronClient/app/gui/NotePropertiesDialog.jsx
index 0c2fe70b1..ff736d272 100644
--- a/ElectronClient/app/gui/NotePropertiesDialog.jsx
+++ b/ElectronClient/app/gui/NotePropertiesDialog.jsx
@@ -2,6 +2,7 @@ const React = require('react');
const { _ } = require('lib/locale.js');
const { themeStyle } = require('../theme.js');
const { time } = require('lib/time-utils.js');
+const DialogButtonRow = require('./DialogButtonRow.min');
const Datetime = require('react-datetime');
const Note = require('lib/models/Note');
const formatcoords = require('formatcoords');
@@ -11,10 +12,8 @@ class NotePropertiesDialog extends React.Component {
constructor() {
super();
- this.okButton_click = this.okButton_click.bind(this);
- this.cancelButton_click = this.cancelButton_click.bind(this);
- this.onKeyDown = this.onKeyDown.bind(this);
this.revisionsLink_click = this.revisionsLink_click.bind(this);
+ this.buttonRow_click = this.buttonRow_click.bind(this);
this.okButton = React.createRef();
this.state = {
@@ -153,12 +152,8 @@ class NotePropertiesDialog extends React.Component {
}
}
- okButton_click() {
- this.closeDialog(true);
- }
-
- cancelButton_click() {
- this.closeDialog(false);
+ buttonRow_click(event) {
+ this.closeDialog(event.buttonName === 'ok');
}
revisionsLink_click() {
@@ -166,14 +161,6 @@ class NotePropertiesDialog extends React.Component {
if (this.props.onRevisionLinkClick) this.props.onRevisionLinkClick();
}
- onKeyDown(event) {
- if (event.keyCode === 13) {
- this.closeDialog(true);
- } else if (event.keyCode === 27) {
- this.closeDialog(false);
- }
- }
-
editPropertyButtonClick(key, initialValue) {
this.setState({
editedKey: key,
@@ -218,15 +205,12 @@ class NotePropertiesDialog extends React.Component {
async cancelProperty() {
return new Promise((resolve) => {
this.okButton.current.focus();
- this.setState(
- {
- editedKey: null,
- editedValue: null,
- },
- () => {
- resolve();
- }
- );
+ this.setState({
+ editedKey: null,
+ editedValue: null,
+ }, () => {
+ resolve();
+ });
});
}
@@ -363,21 +347,8 @@ class NotePropertiesDialog extends React.Component {
render() {
const theme = themeStyle(this.props.theme);
- const styles = this.styles(this.props.theme);
const formNote = this.state.formNote;
- const buttonComps = [];
- buttonComps.push(
-
- {_('Apply')}
-
- );
- buttonComps.push(
-
- {_('Cancel')}
-
- );
-
const noteComps = [];
if (formNote) {
@@ -393,7 +364,7 @@ class NotePropertiesDialog extends React.Component {
{_('Note properties')}
{noteComps}
-
{buttonComps}
+
);
diff --git a/ElectronClient/app/gui/NoteText.jsx b/ElectronClient/app/gui/NoteText.jsx
index 76fd96bbc..7d4843479 100644
--- a/ElectronClient/app/gui/NoteText.jsx
+++ b/ElectronClient/app/gui/NoteText.jsx
@@ -406,7 +406,7 @@ class NoteTextComponent extends React.Component {
return this.markupToHtml_;
}
- async componentWillMount() {
+ async UNSAFE_componentWillMount() {
let note = null;
let noteTags = [];
if (this.props.newNote) {
@@ -685,7 +685,7 @@ class NoteTextComponent extends React.Component {
defer();
}
- async componentWillReceiveProps(nextProps) {
+ async UNSAFE_componentWillReceiveProps(nextProps) {
if (this.props.newNote !== nextProps.newNote && nextProps.newNote) {
await this.scheduleReloadNote(nextProps);
} else if ('noteId' in nextProps && nextProps.noteId !== this.props.noteId) {
diff --git a/ElectronClient/app/gui/OneDriveLoginScreen.jsx b/ElectronClient/app/gui/OneDriveLoginScreen.jsx
index 63559d30c..d0c01d6ab 100644
--- a/ElectronClient/app/gui/OneDriveLoginScreen.jsx
+++ b/ElectronClient/app/gui/OneDriveLoginScreen.jsx
@@ -18,7 +18,7 @@ class OneDriveLoginScreenComponent extends React.Component {
this.webview_.src = this.startUrl();
}
- componentWillMount() {
+ UNSAFE_componentWillMount() {
this.setState({
webviewUrl: this.startUrl(),
webviewReady: false,
diff --git a/ElectronClient/app/gui/PromptDialog.jsx b/ElectronClient/app/gui/PromptDialog.jsx
index 112b5ddb8..9d2127c9e 100644
--- a/ElectronClient/app/gui/PromptDialog.jsx
+++ b/ElectronClient/app/gui/PromptDialog.jsx
@@ -14,7 +14,7 @@ class PromptDialog extends React.Component {
this.answerInput_ = React.createRef();
}
- componentWillMount() {
+ UNSAFE_componentWillMount() {
this.setState({
visible: false,
answer: this.props.defaultValue ? this.props.defaultValue : '',
@@ -22,7 +22,7 @@ class PromptDialog extends React.Component {
this.focusInput_ = true;
}
- componentWillReceiveProps(newProps) {
+ UNSAFE_componentWillReceiveProps(newProps) {
if ('visible' in newProps && newProps.visible !== this.props.visible) {
this.setState({ visible: newProps.visible });
if (newProps.visible) this.focusInput_ = true;
diff --git a/ElectronClient/app/gui/ShareNoteDialog.tsx b/ElectronClient/app/gui/ShareNoteDialog.tsx
new file mode 100644
index 000000000..e70ba8338
--- /dev/null
+++ b/ElectronClient/app/gui/ShareNoteDialog.tsx
@@ -0,0 +1,200 @@
+// const React = require('react');
+// const { useState, useEffect } = React;
+import * as React from 'react';
+import { useState, useEffect } from 'react';
+import JoplinServerApi from '../lib/JoplinServerApi';
+
+const { _, _n } = require('lib/locale.js');
+const { themeStyle, buildStyle } = require('../theme.js');
+const DialogButtonRow = require('./DialogButtonRow.min');
+const Note = require('lib/models/Note');
+const Setting = require('lib/models/Setting');
+const { reg } = require('lib/registry.js');
+const { clipboard } = require('electron');
+
+interface ShareNoteDialogProps {
+ theme: number,
+ noteIds: Array,
+ onClose: Function,
+}
+
+interface SharesMap {
+ [key: string]: any;
+}
+
+function styles_(props:ShareNoteDialogProps) {
+ return buildStyle('ShareNoteDialog', props.theme, (theme:any) => {
+ return {
+ noteList: {
+ marginBottom: 10,
+ },
+ note: {
+ flex: 1,
+ flexDirection: 'row',
+ display: 'flex',
+ alignItems: 'center',
+ border: '1px solid',
+ borderColor: theme.dividerColor,
+ padding: '0.5em',
+ marginBottom: 5,
+ },
+ noteTitle: {
+ flex: 1,
+ display: 'flex',
+ color: theme.color,
+ },
+ noteRemoveButton: {
+ background: 'none',
+ border: 'none',
+ },
+ noteRemoveButtonIcon: {
+ color: theme.color,
+ fontSize: '1.4em',
+ },
+ copyShareLinkButton: {
+ ...theme.buttonStyle,
+ marginBottom: 10,
+ },
+ };
+ });
+}
+
+export default function ShareNoteDialog(props:ShareNoteDialogProps) {
+ console.info('Render ShareNoteDialog');
+
+ const [notes, setNotes] = useState([]);
+ const [sharesState, setSharesState] = useState('unknown');
+ const [shares, setShares] = useState({});
+
+ const noteCount = notes.length;
+ const theme = themeStyle(props.theme);
+ const styles = styles_(props);
+
+ useEffect(() => {
+ async function fetchNotes() {
+ const result = [];
+ for (let noteId of props.noteIds) {
+ result.push(await Note.load(noteId));
+ }
+ setNotes(result);
+ }
+
+ fetchNotes();
+ }, [props.noteIds]);
+
+ const appApi = async () => {
+ return reg.syncTargetNextcloud().appApi();
+ };
+
+ const buttonRow_click = () => {
+ props.onClose();
+ };
+
+ const copyLinksToClipboard = (shares:SharesMap) => {
+ const links = [];
+ for (const n in shares) links.push(shares[n]._url);
+ clipboard.writeText(links.join('\n'));
+ };
+
+ const shareLinkButton_click = async () => {
+ let hasSynced = false;
+ let tryToSync = false;
+ while (true) {
+ try {
+ setSharesState('creating');
+
+ if (tryToSync) {
+ const synchronizer = await reg.syncTarget().synchronizer();
+ await synchronizer.waitForSyncToFinish();
+ await reg.scheduleSync(0);
+ tryToSync = false;
+ hasSynced = true;
+ }
+
+ const api = await appApi();
+ const syncTargetId = api.syncTargetId(Setting.toPlainObject());
+ const newShares = Object.assign({}, shares);
+
+ for (const note of notes) {
+ const result = await api.exec('POST', 'shares', {
+ syncTargetId: syncTargetId,
+ noteId: note.id,
+ });
+ newShares[note.id] = result;
+ }
+
+ setShares(newShares);
+
+ copyLinksToClipboard(newShares);
+
+ setSharesState('created');
+ } catch (error) {
+ if (error.code === 404 && !hasSynced) {
+ reg.logger().info('ShareNoteDialog: Note does not exist on server - trying to sync it.', error);
+ tryToSync = true;
+ continue;
+ }
+
+ reg.logger().error('ShareNoteDialog: Cannot share note:', error);
+
+ setSharesState('idle');
+ alert(JoplinServerApi.connectionErrorMessage(error));
+ }
+
+ break;
+ }
+ };
+
+ const removeNoteButton_click = (event:any) => {
+ const newNotes = [];
+ for (let i = 0; i < notes.length; i++) {
+ const n = notes[i];
+ if (n.id === event.noteId) continue;
+ newNotes.push(n);
+ }
+ setNotes(newNotes);
+ };
+
+ const renderNote = (note:any) => {
+ const removeButton = notes.length <= 1 ? null : (
+ removeNoteButton_click({ noteId: note.id })} style={styles.noteRemoveButton}>
+
+
+ );
+
+ return (
+
+ {note.title} {removeButton}
+
+ );
+ };
+
+ const renderNoteList = (notes:any) => {
+ const noteComps = [];
+ for (let noteId of Object.keys(notes)) {
+ noteComps.push(renderNote(notes[noteId]));
+ }
+ return {noteComps}
;
+ };
+
+ const statusMessage = (sharesState:string):string => {
+ if (sharesState === 'creating') return _n('Generating link...', 'Generating links...', noteCount);
+ if (sharesState === 'created') return _n('Link has been copied to clipboard!', 'Links have been copied to clipboard!', noteCount);
+ return '';
+ };
+
+ const rootStyle = Object.assign({}, theme.dialogBox);
+ rootStyle.width = '50%';
+
+ return (
+
+
+
{_('Share Notes')}
+ {renderNoteList(notes)}
+
{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}
+
{statusMessage(sharesState)}
+
+
+
+ );
+}
diff --git a/ElectronClient/app/gui/StatusScreen.jsx b/ElectronClient/app/gui/StatusScreen.jsx
index 645a35890..006c4ed3b 100644
--- a/ElectronClient/app/gui/StatusScreen.jsx
+++ b/ElectronClient/app/gui/StatusScreen.jsx
@@ -16,7 +16,7 @@ class StatusScreenComponent extends React.Component {
};
}
- componentWillMount() {
+ UNSAFE_componentWillMount() {
this.resfreshScreen();
}
diff --git a/ElectronClient/app/gui/utils/NoteListUtils.js b/ElectronClient/app/gui/utils/NoteListUtils.js
index 03fa83ac3..949e4c4e9 100644
--- a/ElectronClient/app/gui/utils/NoteListUtils.js
+++ b/ElectronClient/app/gui/utils/NoteListUtils.js
@@ -107,6 +107,20 @@ class NoteListUtils {
})
);
+ menu.append(
+ new MenuItem({
+ label: _('Share note...'),
+ click: async () => {
+ console.info('NOTE IDS', noteIds);
+ props.dispatch({
+ type: 'WINDOW_COMMAND',
+ name: 'commandShareNoteDialog',
+ noteIds: noteIds.slice(),
+ });
+ },
+ })
+ );
+
const exportMenu = new Menu();
const ioService = new InteropService();
diff --git a/ElectronClient/app/package-lock.json b/ElectronClient/app/package-lock.json
index d999c603d..b9d15fec3 100644
--- a/ElectronClient/app/package-lock.json
+++ b/ElectronClient/app/package-lock.json
@@ -143,6 +143,22 @@
"integrity": "sha512-Ja7d4s0qyGFxjGeDq5S7Si25OFibSAHUi6i17UWnwNnpitADN7hah9q0Tl25gxuV5R1u2Bx+np6w4LHXfHyj/g==",
"dev": true
},
+ "@types/prop-types": {
+ "version": "15.7.3",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
+ "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==",
+ "dev": true
+ },
+ "@types/react": {
+ "version": "16.9.16",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.16.tgz",
+ "integrity": "sha512-dQ3wlehuBbYlfvRXfF5G+5TbZF3xqgkikK7DWAsQXe2KnzV+kjD4W2ea+ThCrKASZn9h98bjjPzoTYzfRqyBkw==",
+ "dev": true,
+ "requires": {
+ "@types/prop-types": "*",
+ "csstype": "^2.2.0"
+ }
+ },
"abab": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.2.tgz",
@@ -596,12 +612,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -616,12 +634,14 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"console-control-strings": {
"version": "1.1.0",
@@ -743,7 +763,8 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"ini": {
"version": "1.3.5",
@@ -755,6 +776,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -769,6 +791,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -776,7 +799,8 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"minipass": {
"version": "2.3.5",
@@ -880,7 +904,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"object-assign": {
"version": "4.1.1",
@@ -5548,14 +5573,13 @@
}
},
"react": {
- "version": "16.8.1",
- "resolved": "https://registry.npmjs.org/react/-/react-16.8.1.tgz",
- "integrity": "sha512-wLw5CFGPdo7p/AgteFz7GblI2JPOos0+biSoxf1FPsGxWQZdN/pj6oToJs1crn61DL3Ln7mN86uZ4j74p31ELQ==",
+ "version": "16.12.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz",
+ "integrity": "sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
- "prop-types": "^15.6.2",
- "scheduler": "^0.13.1"
+ "prop-types": "^15.6.2"
},
"dependencies": {
"prop-types": {
@@ -5611,14 +5635,36 @@
}
},
"react-dom": {
- "version": "16.4.0",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.4.0.tgz",
- "integrity": "sha512-bbLd+HYpBEnYoNyxDe9XpSG2t9wypMohwQPvKw8Hov3nF7SJiJIgK56b46zHpBUpHb06a1iEuw7G3rbrsnNL6w==",
+ "version": "16.12.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.12.0.tgz",
+ "integrity": "sha512-LMxFfAGrcS3kETtQaCkTKjMiifahaMySFDn71fZUNpPHZQEzmk/GiAeIT8JSOrHB23fnuCOMruL2a8NYlw+8Gw==",
"requires": {
- "fbjs": "^0.8.16",
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
- "prop-types": "^15.6.0"
+ "prop-types": "^15.6.2",
+ "scheduler": "^0.18.0"
+ },
+ "dependencies": {
+ "prop-types": {
+ "version": "15.7.2",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
+ "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
+ "requires": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.8.1"
+ },
+ "dependencies": {
+ "loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "requires": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ }
+ }
+ }
+ }
}
},
"react-input-autosize": {
@@ -6073,9 +6119,9 @@
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"scheduler": {
- "version": "0.13.6",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz",
- "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==",
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.18.0.tgz",
+ "integrity": "sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
diff --git a/ElectronClient/app/package.json b/ElectronClient/app/package.json
index 8acf455c6..0659ad604 100644
--- a/ElectronClient/app/package.json
+++ b/ElectronClient/app/package.json
@@ -9,7 +9,8 @@
"dist": "node_modules/.bin/electron-builder",
"publish": "build -p always",
"postinstall": "node compile.js && node compile-package-info.js && node ../../Tools/copycss.js --copy-fonts && node electronRebuild.js",
- "compile": "node compile.js && node compile-package-info.js && node ../../Tools/copycss.js --copy-fonts"
+ "compile": "node compile.js && node compile-package-info.js && node ../../Tools/copycss.js --copy-fonts",
+ "install-141": "npm install --toolset=v141"
},
"repository": {
"type": "git",
@@ -133,10 +134,10 @@
"node-notifier": "^6.0.0",
"promise": "^8.0.1",
"query-string": "^5.1.1",
- "react": "^16.4.0",
+ "react": "^16.12.0",
"react-ace": "^6.1.4",
"react-datetime": "^2.14.0",
- "react-dom": "^16.4.0",
+ "react-dom": "^16.12.0",
"react-redux": "^5.0.7",
"react-select": "^2.4.3",
"react-tooltip": "^3.10.0",
diff --git a/ElectronClient/app/theme.js b/ElectronClient/app/theme.js
index be2cd01c9..c12344664 100644
--- a/ElectronClient/app/theme.js
+++ b/ElectronClient/app/theme.js
@@ -63,7 +63,7 @@ globalStyle.buttonStyle = {
border: '1px solid',
minHeight: 26,
minWidth: 80,
- maxWidth: 160,
+ maxWidth: 220,
paddingLeft: 12,
paddingRight: 12,
paddingTop: 6,
@@ -471,4 +471,19 @@ function themeStyle(theme) {
return themeCache_[cacheKey];
}
-module.exports = { themeStyle };
+const cachedStyles_ = {};
+
+function buildStyle(cacheKey, themeId, callback) {
+ if (cachedStyles_[cacheKey]) cachedStyles_[cacheKey].style;
+
+ const s = callback(themeStyle(themeId));
+
+ cachedStyles_[cacheKey] = {
+ style: s,
+ timestamp: Date.now(),
+ };
+
+ return cachedStyles_[cacheKey].style;
+}
+
+module.exports = { themeStyle, buildStyle };
diff --git a/ReactNativeClient/lib/JoplinServerApi.ts b/ReactNativeClient/lib/JoplinServerApi.ts
new file mode 100644
index 000000000..cb244a863
--- /dev/null
+++ b/ReactNativeClient/lib/JoplinServerApi.ts
@@ -0,0 +1,158 @@
+const { Logger } = require('lib/logger.js');
+const { shim } = require('lib/shim.js');
+const JoplinError = require('lib/JoplinError');
+const { rtrimSlashes } = require('lib/path-utils.js');
+const base64 = require('base-64');
+const {_ } = require('lib/locale');
+
+interface JoplinServerApiOptions {
+ username: Function,
+ password: Function,
+ baseUrl: Function,
+}
+
+export default class JoplinServerApi {
+
+ logger_:any;
+ options_:JoplinServerApiOptions;
+ kvStore_:any;
+
+ constructor(options:JoplinServerApiOptions) {
+ this.logger_ = new Logger();
+ this.options_ = options;
+ this.kvStore_ = null;
+ }
+
+ setLogger(l:any) {
+ this.logger_ = l;
+ }
+
+ logger():any {
+ return this.logger_;
+ }
+
+ setKvStore(v:any) {
+ this.kvStore_ = v;
+ }
+
+ kvStore() {
+ if (!this.kvStore_) throw new Error('JoplinServerApi.kvStore_ is not set!!');
+ return this.kvStore_;
+ }
+
+ authToken():string {
+ if (!this.options_.username() || !this.options_.password()) return null;
+ try {
+ // Note: Non-ASCII passwords will throw an error about Latin1 characters - https://github.com/laurent22/joplin/issues/246
+ // Tried various things like the below, but it didn't work on React Native:
+ // return base64.encode(utf8.encode(this.options_.username() + ':' + this.options_.password()));
+ return base64.encode(`${this.options_.username()}:${this.options_.password()}`);
+ } catch (error) {
+ error.message = `Cannot encode username/password: ${error.message}`;
+ throw error;
+ }
+ }
+
+ baseUrl():string {
+ return rtrimSlashes(this.options_.baseUrl());
+ }
+
+ static baseUrlFromNextcloudWebDavUrl(webDavUrl:string) {
+ // http://nextcloud.local/remote.php/webdav/Joplin
+ // http://nextcloud.local/index.php/apps/joplin/api
+ const splitted = webDavUrl.split('/remote.php/webdav');
+ if (splitted.length !== 2) throw new Error(`Unsupported WebDAV URL format: ${webDavUrl}`);
+ return `${splitted[0]}/index.php/apps/joplin/api`;
+ }
+
+ syncTargetId(settings:any) {
+ const s = settings['sync.5.syncTargets'][settings['sync.5.path']];
+ if (!s) throw new Error(`Joplin Nextcloud app not configured for URL: ${this.baseUrl()}`);
+ return s.uuid;
+ }
+
+ static connectionErrorMessage(error:any) {
+ const msg = error && error.message ? error.message : 'Unknown error';
+ return _('Could not connect to the Joplin Nextcloud app. Please check the configuration in the Synchronisation config screen. Full error was:\n\n%s', msg);
+ }
+
+ async setupSyncTarget(webDavUrl:string) {
+ return this.exec('POST', 'sync_targets', {
+ webDavUrl: webDavUrl,
+ });
+ }
+
+ requestToCurl_(url:string, options:any) {
+ let output = [];
+ output.push('curl');
+ output.push('-v');
+ if (options.method) output.push(`-X ${options.method}`);
+ if (options.headers) {
+ for (let n in options.headers) {
+ if (!options.headers.hasOwnProperty(n)) continue;
+ output.push(`${'-H ' + '"'}${n}: ${options.headers[n]}"`);
+ }
+ }
+ if (options.body) output.push(`${'--data ' + '\''}${options.body}'`);
+ output.push(url);
+
+ return output.join(' ');
+ }
+
+ async exec(method:string, path:string = '', body:any = null, headers:any = null, options:any = null):Promise {
+ if (headers === null) headers = {};
+ if (options === null) options = {};
+
+ const authToken = this.authToken();
+
+ if (authToken) headers['Authorization'] = `Basic ${authToken}`;
+
+ headers['Content-Type'] = 'application/json';
+
+ if (typeof body === 'object' && body !== null) body = JSON.stringify(body);
+
+ const fetchOptions:any = {};
+ fetchOptions.headers = headers;
+ fetchOptions.method = method;
+ if (options.path) fetchOptions.path = options.path;
+ if (body) fetchOptions.body = body;
+
+ const url = `${this.baseUrl()}/${path}`;
+
+ let response = null;
+
+ // console.info('WebDAV Call', method + ' ' + url, headers, options);
+ console.info(this.requestToCurl_(url, fetchOptions));
+
+ if (typeof body === 'string') fetchOptions.headers['Content-Length'] = `${shim.stringByteLength(body)}`;
+ response = await shim.fetch(url, fetchOptions);
+
+ const responseText = await response.text();
+
+ let responseJson_:any = null;
+ const loadResponseJson = async () => {
+ if (!responseText) return null;
+ if (responseJson_) return responseJson_;
+ return JSON.parse(responseText);
+ };
+
+ const newError = (message:string, code:number = 0) => {
+ return new JoplinError(`${method} ${path}: ${message} (${code})`, code);
+ };
+
+ if (!response.ok) {
+ let json = null;
+ try {
+ json = await loadResponseJson();
+ } catch (error) {
+ throw newError(`Unknown error: ${responseText.substr(0, 4096)}`, response.status);
+ }
+
+ const trace = json.stacktrace ? `\n${json.stacktrace}` : '';
+ throw newError(json.error + trace, response.status);
+ }
+
+ const output = await loadResponseJson();
+ return output;
+ }
+}
diff --git a/ReactNativeClient/lib/SyncTargetNextcloud.js b/ReactNativeClient/lib/SyncTargetNextcloud.js
index f63811593..2e01b4741 100644
--- a/ReactNativeClient/lib/SyncTargetNextcloud.js
+++ b/ReactNativeClient/lib/SyncTargetNextcloud.js
@@ -6,8 +6,10 @@ const { _ } = require('lib/locale.js');
const Setting = require('lib/models/Setting.js');
const { Synchronizer } = require('lib/synchronizer.js');
const SyncTargetWebDAV = require('lib/SyncTargetWebDAV');
+const JoplinServerApi = require('lib/JoplinServerApi.js').default;
class SyncTargetNextcloud extends BaseSyncTarget {
+
static id() {
return 5;
}
@@ -47,6 +49,21 @@ class SyncTargetNextcloud extends BaseSyncTarget {
async initSynchronizer() {
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
}
+
+ async appApi() {
+ if (!this.appApi_) {
+ this.appApi_ = new JoplinServerApi({
+ baseUrl: () => JoplinServerApi.baseUrlFromNextcloudWebDavUrl(Setting.value('sync.5.path')),
+ username: () => Setting.value('sync.5.username'),
+ password: () => Setting.value('sync.5.password'),
+ });
+
+ this.appApi_.setLogger(this.logger());
+ }
+
+ return this.appApi_;
+ }
+
}
module.exports = SyncTargetNextcloud;
diff --git a/ReactNativeClient/lib/components/shared/config-shared.js b/ReactNativeClient/lib/components/shared/config-shared.js
index d738f20a2..3e33f8923 100644
--- a/ReactNativeClient/lib/components/shared/config-shared.js
+++ b/ReactNativeClient/lib/components/shared/config-shared.js
@@ -3,14 +3,24 @@ const SyncTargetRegistry = require('lib/SyncTargetRegistry');
const ObjectUtils = require('lib/ObjectUtils');
const { _ } = require('lib/locale.js');
const { createSelector } = require('reselect');
+const { reg } = require('lib/registry');
const shared = {};
shared.init = function(comp) {
if (!comp.state) comp.state = {};
comp.state.checkSyncConfigResult = null;
+ comp.state.checkNextcloudAppResult = null;
comp.state.settings = {};
comp.state.changedSettingKeys = [];
+ comp.state.showNextcloudAppLog = false;
+ comp.state.showAdvancedSettings = false;
+};
+
+shared.advancedSettingsButton_click = (comp) => {
+ comp.setState(state => {
+ return { showAdvancedSettings: !state.showAdvancedSettings };
+ });
};
shared.checkSyncConfig = async function(comp, settings) {
@@ -20,6 +30,12 @@ shared.checkSyncConfig = async function(comp, settings) {
comp.setState({ checkSyncConfigResult: 'checking' });
const result = await SyncTargetClass.checkConfig(ObjectUtils.convertValuesToFunctions(options));
comp.setState({ checkSyncConfigResult: result });
+
+ if (result.ok) {
+ await shared.checkNextcloudApp(comp, settings);
+ // Users often expect config to be auto-saved at this point, if the config check was successful
+ shared.saveSettings(comp);
+ }
};
shared.checkSyncConfigMessages = function(comp) {
@@ -38,6 +54,28 @@ shared.checkSyncConfigMessages = function(comp) {
return output;
};
+shared.checkNextcloudApp = async function(comp, settings) {
+ comp.setState({ checkNextcloudAppResult: 'checking' });
+ let result = null;
+ const appApi = await reg.syncTargetNextcloud().appApi();
+
+ try {
+ result = await appApi.setupSyncTarget(settings['sync.5.path']);
+ } catch (error) {
+ reg.logger().error('Could not setup sync target:', error);
+ result = { error: error.message };
+ }
+
+ const newSyncTargets = Object.assign({}, settings['sync.5.syncTargets']);
+ newSyncTargets[settings['sync.5.path']] = result;
+ shared.updateSettingValue(comp, 'sync.5.syncTargets', newSyncTargets);
+
+ // Also immediately save the result as this is most likely what the user would expect
+ Setting.setValue('sync.5.syncTargets', newSyncTargets);
+
+ comp.setState({ checkNextcloudAppResult: 'done' });
+};
+
shared.updateSettingValue = function(comp, key, value) {
const settings = Object.assign({}, comp.state.settings);
const changedSettingKeys = comp.state.changedSettingKeys.slice();
diff --git a/ReactNativeClient/lib/models/Setting.js b/ReactNativeClient/lib/models/Setting.js
index e10151cf4..20ec3411e 100644
--- a/ReactNativeClient/lib/models/Setting.js
+++ b/ReactNativeClient/lib/models/Setting.js
@@ -162,11 +162,14 @@ class Setting extends BaseModel {
'sync.6.context': { value: '', type: Setting.TYPE_STRING, public: false },
'sync.7.context': { value: '', type: Setting.TYPE_STRING, public: false },
+ 'sync.5.syncTargets': { value: {}, type: Setting.TYPE_OBJECT, public: false },
+
'sync.resourceDownloadMode': {
value: 'always',
type: Setting.TYPE_STRING,
section: 'sync',
public: true,
+ advanced: true,
isEnum: true,
appTypes: ['mobile', 'desktop'],
label: () => _('Attachment download behaviour'),
@@ -180,7 +183,7 @@ class Setting extends BaseModel {
},
},
- 'sync.maxConcurrentConnections': { value: 5, type: Setting.TYPE_INT, public: true, section: 'sync', label: () => _('Max concurrent connections'), minimum: 1, maximum: 20, step: 1 },
+ 'sync.maxConcurrentConnections': { value: 5, type: Setting.TYPE_INT, public: true, advanced: true, section: 'sync', label: () => _('Max concurrent connections'), minimum: 1, maximum: 20, step: 1 },
activeFolderId: { value: '', type: Setting.TYPE_STRING, public: false },
firstStart: { value: true, type: Setting.TYPE_BOOL, public: false },
@@ -497,6 +500,7 @@ class Setting extends BaseModel {
value: '',
type: Setting.TYPE_STRING,
section: 'sync',
+ advanced: true,
show: settings => {
return [SyncTargetRegistry.nameToId('nextcloud'), SyncTargetRegistry.nameToId('webdav')].indexOf(settings['sync.target']) >= 0;
},
@@ -508,6 +512,7 @@ class Setting extends BaseModel {
'net.ignoreTlsErrors': {
value: false,
type: Setting.TYPE_BOOL,
+ advanced: true,
section: 'sync',
show: settings => {
return [SyncTargetRegistry.nameToId('nextcloud'), SyncTargetRegistry.nameToId('webdav')].indexOf(settings['sync.target']) >= 0;
@@ -517,7 +522,7 @@ class Setting extends BaseModel {
label: () => _('Ignore TLS certificate errors'),
},
- 'sync.wipeOutFailSafe': { value: true, type: Setting.TYPE_BOOL, public: true, section: 'sync', label: () => _('Fail-safe: Do not wipe out local data when sync target is empty (often the result of a misconfiguration or bug)') },
+ 'sync.wipeOutFailSafe': { value: true, type: Setting.TYPE_BOOL, advanced: true, public: true, section: 'sync', label: () => _('Fail-safe: Do not wipe out local data when sync target is empty (often the result of a misconfiguration or bug)') },
'api.token': { value: null, type: Setting.TYPE_STRING, public: false },
'api.port': { value: null, type: Setting.TYPE_INT, public: true, appTypes: ['cli'], description: () => _('Specify the port that should be used by the API server. If not set, a default will be used.') },
diff --git a/ReactNativeClient/lib/registry.js b/ReactNativeClient/lib/registry.js
index bb7a5998a..d637ab3d1 100644
--- a/ReactNativeClient/lib/registry.js
+++ b/ReactNativeClient/lib/registry.js
@@ -35,6 +35,10 @@ reg.resetSyncTarget = (syncTargetId = null) => {
delete reg.syncTargets_[syncTargetId];
};
+reg.syncTargetNextcloud = () => {
+ return reg.syncTarget(SyncTargetRegistry.nameToId('nextcloud'));
+};
+
reg.syncTarget = (syncTargetId = null) => {
if (syncTargetId === null) syncTargetId = Setting.value('sync.target');
if (reg.syncTargets_[syncTargetId]) return reg.syncTargets_[syncTargetId];
diff --git a/ReactNativeClient/lib/synchronizer.js b/ReactNativeClient/lib/synchronizer.js
index ca60f7b0f..33e830da9 100644
--- a/ReactNativeClient/lib/synchronizer.js
+++ b/ReactNativeClient/lib/synchronizer.js
@@ -80,6 +80,15 @@ class Synchronizer {
return this.encryptionService_;
}
+ async waitForSyncToFinish() {
+ if (this.state() === 'idle') return;
+
+ while (true) {
+ await time.sleep(1);
+ if (this.state() === 'idle') return;
+ }
+ }
+
static reportToLines(report) {
let lines = [];
if (report.createLocal) lines.push(_('Created local items: %d.', report.createLocal));
diff --git a/appveyor.yml b/appveyor.yml
index 0a81843e0..288f0e9db 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -13,10 +13,12 @@ install:
- yarn
build_script:
+ - ps: xcopy /C /I /H /R /Y /S ReactNativeClient\lib ElectronClient\app\lib
+ - npm install
+ - npm run typescript-compile
- ps: cd Tools
- npm install
- ps: cd ..\ElectronClient\app
- - ps: xcopy /C /I /H /R /Y /S ..\..\ReactNativeClient\lib lib
- npm install
- yarn dist
diff --git a/joplin.code-workspace b/joplin.code-workspace
index 498e557f8..133feaac1 100644
--- a/joplin.code-workspace
+++ b/joplin.code-workspace
@@ -1,7 +1,11 @@
{
"folders": [
{
+ "name": "Joplin",
"path": ".",
+ }, {
+ "name": "Joplin Nextcloud App",
+ "path": "D:/Web/www/nextcloud/apps/joplin",
},
],
"settings": {
diff --git a/package-lock.json b/package-lock.json
index ce9419f96..bc467a9e3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -62,7 +62,8 @@
"@types/eslint-visitor-keys": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
- "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag=="
+ "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==",
+ "dev": true
},
"@types/events": {
"version": "3.0.0",
@@ -84,7 +85,8 @@
"@types/json-schema": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz",
- "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A=="
+ "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==",
+ "dev": true
},
"@types/minimatch": {
"version": "3.0.3",
@@ -111,9 +113,9 @@
"dev": true
},
"@types/react": {
- "version": "16.9.15",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.15.tgz",
- "integrity": "sha512-WsmM1b6xQn1tG3X2Hx4F3bZwc2E82pJXt5OPs2YJgg71IzvUoKOSSSYOvLXYCg1ttipM+UuA4Lj3sfvqjVxyZw==",
+ "version": "16.9.16",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.16.tgz",
+ "integrity": "sha512-dQ3wlehuBbYlfvRXfF5G+5TbZF3xqgkikK7DWAsQXe2KnzV+kjD4W2ea+ThCrKASZn9h98bjjPzoTYzfRqyBkw==",
"dev": true,
"requires": {
"@types/prop-types": "*",
@@ -130,64 +132,147 @@
}
},
"@typescript-eslint/eslint-plugin": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.2.0.tgz",
- "integrity": "sha512-rOodtI+IvaO8USa6ValYOrdWm9eQBgqwsY+B0PPiB+aSiK6p6Z4l9jLn/jI3z3WM4mkABAhKIqvGIBl0AFRaLQ==",
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.10.0.tgz",
+ "integrity": "sha512-rT51fNLW0u3fnDGnAHVC5nu+Das+y2CpW10yqvf6/j5xbuUV3FxA3mBaIbM24CXODXjbgUznNb4Kg9XZOUxKAw==",
+ "dev": true,
"requires": {
- "@typescript-eslint/experimental-utils": "2.2.0",
- "eslint-utils": "^1.4.2",
+ "@typescript-eslint/experimental-utils": "2.10.0",
+ "eslint-utils": "^1.4.3",
"functional-red-black-tree": "^1.0.1",
- "regexpp": "^2.0.1",
+ "regexpp": "^3.0.0",
"tsutils": "^3.17.1"
},
"dependencies": {
- "eslint-utils": {
- "version": "1.4.2",
- "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz",
- "integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==",
+ "@typescript-eslint/experimental-utils": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.10.0.tgz",
+ "integrity": "sha512-FZhWq6hWWZBP76aZ7bkrfzTMP31CCefVIImrwP3giPLcoXocmLTmr92NLZxuIcTL4GTEOE33jQMWy9PwelL+yQ==",
+ "dev": true,
"requires": {
- "eslint-visitor-keys": "^1.0.0"
+ "@types/json-schema": "^7.0.3",
+ "@typescript-eslint/typescript-estree": "2.10.0",
+ "eslint-scope": "^5.0.0"
}
+ },
+ "@typescript-eslint/typescript-estree": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.10.0.tgz",
+ "integrity": "sha512-oOYnplddQNm/LGVkqbkAwx4TIBuuZ36cAQq9v3nFIU9FmhemHuVzAesMSXNQDdAzCa5bFgCrfD3JWhYVKlRN2g==",
+ "dev": true,
+ "requires": {
+ "debug": "^4.1.1",
+ "eslint-visitor-keys": "^1.1.0",
+ "glob": "^7.1.6",
+ "is-glob": "^4.0.1",
+ "lodash.unescape": "4.0.1",
+ "semver": "^6.3.0",
+ "tsutils": "^3.17.1"
+ }
+ },
+ "eslint-utils": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz",
+ "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==",
+ "dev": true,
+ "requires": {
+ "eslint-visitor-keys": "^1.1.0"
+ }
+ },
+ "eslint-visitor-keys": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
+ "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
+ "dev": true
+ },
+ "glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "regexpp": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz",
+ "integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==",
+ "dev": true
}
}
},
"@typescript-eslint/experimental-utils": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.2.0.tgz",
- "integrity": "sha512-IMhbewFs27Frd/ICHBRfIcsUCK213B8MsEUqvKFK14SDPjPR5JF6jgOGPlroybFTrGWpMvN5tMZdXAf+xcmxsA==",
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.10.0.tgz",
+ "integrity": "sha512-FZhWq6hWWZBP76aZ7bkrfzTMP31CCefVIImrwP3giPLcoXocmLTmr92NLZxuIcTL4GTEOE33jQMWy9PwelL+yQ==",
+ "dev": true,
"requires": {
"@types/json-schema": "^7.0.3",
- "@typescript-eslint/typescript-estree": "2.2.0",
+ "@typescript-eslint/typescript-estree": "2.10.0",
"eslint-scope": "^5.0.0"
}
},
"@typescript-eslint/parser": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.2.0.tgz",
- "integrity": "sha512-0mf893kj9L65O5sA7wP6EoYvTybefuRFavUNhT7w9kjhkdZodoViwVS+k3D+ZxKhvtL7xGtP/y/cNMJX9S8W4A==",
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.10.0.tgz",
+ "integrity": "sha512-wQNiBokcP5ZsTuB+i4BlmVWq6o+oAhd8en2eSm/EE9m7BgZUIfEeYFd6z3S+T7bgNuloeiHA1/cevvbBDLr98g==",
+ "dev": true,
"requires": {
"@types/eslint-visitor-keys": "^1.0.0",
- "@typescript-eslint/experimental-utils": "2.2.0",
- "@typescript-eslint/typescript-estree": "2.2.0",
+ "@typescript-eslint/experimental-utils": "2.10.0",
+ "@typescript-eslint/typescript-estree": "2.10.0",
"eslint-visitor-keys": "^1.1.0"
},
"dependencies": {
"eslint-visitor-keys": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
- "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A=="
+ "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
+ "dev": true
}
}
},
"@typescript-eslint/typescript-estree": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.2.0.tgz",
- "integrity": "sha512-9/6x23A3HwWWRjEQbuR24on5XIfVmV96cDpGR9671eJv1ebFKHj2sGVVAwkAVXR2UNuhY1NeKS2QMv5P8kQb2Q==",
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.10.0.tgz",
+ "integrity": "sha512-oOYnplddQNm/LGVkqbkAwx4TIBuuZ36cAQq9v3nFIU9FmhemHuVzAesMSXNQDdAzCa5bFgCrfD3JWhYVKlRN2g==",
+ "dev": true,
"requires": {
- "glob": "^7.1.4",
+ "debug": "^4.1.1",
+ "eslint-visitor-keys": "^1.1.0",
+ "glob": "^7.1.6",
"is-glob": "^4.0.1",
"lodash.unescape": "4.0.1",
- "semver": "^6.3.0"
+ "semver": "^6.3.0",
+ "tsutils": "^3.17.1"
+ },
+ "dependencies": {
+ "eslint-visitor-keys": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
+ "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
+ "dev": true
+ },
+ "glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ }
}
},
"acorn": {
@@ -275,12 +360,14 @@
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
- "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "dev": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -448,7 +535,8 @@
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
},
"cosmiconfig": {
"version": "5.2.1",
@@ -712,6 +800,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz",
"integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==",
+ "dev": true,
"requires": {
"esrecurse": "^4.1.0",
"estraverse": "^4.1.1"
@@ -729,7 +818,8 @@
"eslint-visitor-keys": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
- "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ=="
+ "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==",
+ "dev": true
},
"espree": {
"version": "6.0.0",
@@ -761,6 +851,7 @@
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
"integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+ "dev": true,
"requires": {
"estraverse": "^4.1.0"
}
@@ -768,7 +859,8 @@
"estraverse": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
- "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM="
+ "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
+ "dev": true
},
"esutils": {
"version": "2.0.2",
@@ -900,7 +992,8 @@
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
},
"function-bind": {
"version": "1.1.1",
@@ -911,7 +1004,8 @@
"functional-red-black-tree": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
- "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc="
+ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
+ "dev": true
},
"get-own-enumerable-property-symbols": {
"version": "3.0.0",
@@ -932,6 +1026,7 @@
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz",
"integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==",
+ "dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -1092,6 +1187,7 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
@@ -1100,7 +1196,8 @@
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
},
"inquirer": {
"version": "6.5.0",
@@ -1159,7 +1256,8 @@
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "dev": true
},
"is-fullwidth-code-point": {
"version": "2.0.0",
@@ -1171,6 +1269,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+ "dev": true,
"requires": {
"is-extglob": "^2.1.1"
}
@@ -1543,7 +1642,8 @@
"lodash.unescape": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz",
- "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw="
+ "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=",
+ "dev": true
},
"log-symbols": {
"version": "3.0.0",
@@ -1606,6 +1706,7 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -1748,6 +1849,7 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
"requires": {
"wrappy": "1"
}
@@ -1851,7 +1953,8 @@
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
- "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
},
"path-is-inside": {
"version": "1.0.2",
@@ -1975,7 +2078,8 @@
"regexpp": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
- "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw=="
+ "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
+ "dev": true
},
"resolve": {
"version": "1.11.1",
@@ -2056,7 +2160,8 @@
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true
},
"semver-compare": {
"version": "1.0.0",
@@ -2286,12 +2391,14 @@
"tslib": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
- "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ=="
+ "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==",
+ "dev": true
},
"tsutils": {
"version": "3.17.1",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz",
"integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==",
+ "dev": true,
"requires": {
"tslib": "^1.8.1"
}
@@ -2381,7 +2488,8 @@
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
},
"write": {
"version": "1.0.3",
diff --git a/package.json b/package.json
index 8a72c3129..5b6ebac2b 100644
--- a/package.json
+++ b/package.json
@@ -3,8 +3,10 @@
"version": "1.0.0",
"description": "Joplin root package for linting",
"scripts": {
- "linter": "./node_modules/.bin/eslint --fix --ext .js --ext .jsx",
- "linter-ci": "./node_modules/.bin/eslint --ext .js --ext .jsx"
+ "linter": "./node_modules/.bin/eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx",
+ "linter-ci": "./node_modules/.bin/eslint --ext .js --ext .jsx --ext .ts --ext .tsx",
+ "typescript-compile": "tsc",
+ "typescript-watch": "tsc --watch"
},
"husky": {
"hooks": {
@@ -12,7 +14,7 @@
}
},
"lint-staged": {
- "*.{js,jsx}": [
+ "*.{js,jsx,ts,tsx}": [
"npm run linter",
"git add"
]
@@ -23,16 +25,14 @@
},
"license": "MIT",
"devDependencies": {
- "@types/react": "^16.9.15",
+ "@types/react": "^16.9.16",
"@types/react-dom": "^16.9.4",
+ "@typescript-eslint/eslint-plugin": "^2.10.0",
+ "@typescript-eslint/parser": "^2.10.0",
"eslint": "^6.1.0",
"eslint-plugin-react": "^7.14.3",
"husky": "^3.0.2",
"lint-staged": "^9.2.1",
"typescript": "^3.7.3"
- },
- "dependencies": {
- "@typescript-eslint/eslint-plugin": "^2.2.0",
- "@typescript-eslint/parser": "^2.2.0"
}
}
diff --git a/readme/nextcloud_app.md b/readme/nextcloud_app.md
new file mode 100644
index 000000000..715d2e701
--- /dev/null
+++ b/readme/nextcloud_app.md
@@ -0,0 +1,10 @@
+# Nextcloud App
+
+**This is a beta feature, not yet completed. More info coming soon!**
+
+The Joplin Nextcloud App is a helper application that enables certain features that are not possible otherwise. In particular:
+
+- [Done] Sharing a note publicly
+- [Not done] Sharing a note with another Joplin user (who uses the same Nextcloud instance)
+- [Not done] Collaborating on a note
+- [Not done] Sharing a notebook
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index 54fe85c86..209e96160 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
- "target": "es2018",
+ "target": "es2015",
"alwaysStrict": true,
"forceConsistentCasingInFileNames": true,
"listEmittedFiles": true,
@@ -16,10 +16,12 @@
"jsx": "react",
},
"include": [
- "ReactNativeClient/lib/**/*.ts",
- "ReactNativeClient/lib/**/*.tsx",
+ "ReactNativeClient/**/*.ts",
+ "ReactNativeClient/**/*.tsx",
"ElectronClient/**/*.ts",
"ElectronClient/**/*.tsx",
+ "CliClient/**/*.ts",
+ "CliClient/**/*.tsx",
],
"exclude": [
"**/node_modules",